Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From version-0.0.13 To version-0.0.14
2021-07-23
| ||
16:43 | Increase version to 0.0.15-dev to begin next development cycle ... (check-in: ba65777850 user: stern tags: trunk) | |
11:02 | Version 0.0.14 ... (check-in: 6fe53d5db2 user: stern tags: trunk, release, version-0.0.14) | |
2021-07-22
| ||
13:50 | Add client API for retrieving zettel links ... (check-in: e43ff68174 user: stern tags: trunk) | |
2021-06-07
| ||
09:11 | Increase version to 0.0.14-dev to begin next development cycle ... (check-in: 7dd6f4dd5c user: stern tags: trunk) | |
2021-06-01
| ||
12:35 | Version 0.0.13 ... (check-in: 11d9b6da63 user: stern tags: trunk, release, version-0.0.13) | |
10:14 | Log output while starting Command Line Server ... (check-in: 968a91bbaa user: stern tags: trunk) | |
Changes to Makefile.
1 2 3 4 5 6 7 8 9 | ## Copyright (c) 2020-2021 Detlef Stern ## ## This file is part of zettelstore. ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. | | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | ## Copyright (c) 2020-2021 Detlef Stern ## ## This file is part of zettelstore. ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. .PHONY: check api build release clean check: go run tools/build.go check api: go run tools/build.go testapi version: @echo $(shell go run tools/build.go version) build: go run tools/build.go build release: go run tools/build.go release clean: go run tools/build.go clean |
Changes to VERSION.
|
| | | 1 | 0.0.14 |
Added api/api.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api contains common definition used for client and server. package api // AuthJSON contains the result of an authentication call. type AuthJSON struct { Token string `json:"token"` Type string `json:"token_type"` Expires int `json:"expires_in"` } // ZidJSON contains the identifier data of a zettel. type ZidJSON struct { ID string `json:"id"` URL string `json:"url"` } // ZidMetaJSON contains the identifier and the metadata of a zettel. type ZidMetaJSON struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` } // ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel type ZidMetaRelatedList struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` List []ZidMetaJSON `json:"list"` } // ZettelLinksJSON store all links / connections from one zettel to other. type ZettelLinksJSON struct { ID string `json:"id"` URL string `json:"url"` Links struct { Incoming []ZidJSON `json:"incoming,omitempty"` Outgoing []ZidJSON `json:"outgoing,omitempty"` Local []string `json:"local,omitempty"` External []string `json:"external,omitempty"` Meta []string `json:"meta,omitempty"` } `json:"links"` Images struct { Outgoing []ZidJSON `json:"outgoing,omitempty"` Local []string `json:"local,omitempty"` External []string `json:"external,omitempty"` } `json:"images,omitempty"` Cites []string `json:"cites,omitempty"` } // ZettelDataJSON contains all data for a zettel. type ZettelDataJSON struct { Meta map[string]string `json:"meta"` Encoding string `json:"encoding"` Content string `json:"content"` } // ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content. type ZettelJSON struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` Encoding string `json:"encoding"` Content string `json:"content"` } // ZettelListJSON contains data for a zettel list. type ZettelListJSON struct { List []ZettelJSON `json:"list"` } // TagListJSON specifies the list/map of tags type TagListJSON struct { Tags map[string][]string `json:"tags"` } // RoleListJSON specifies the list of roles. type RoleListJSON struct { Roles []string `json:"role-list"` } |
Added api/const.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api contains common definition used for client and server. package api import ( "fmt" ) // Additional HTTP constants used. const ( MethodMove = "MOVE" // HTTP method for renaming a zettel HeaderAccept = "Accept" HeaderContentType = "Content-Type" HeaderDestination = "Destination" HeaderLocation = "Location" ) // Values for HTTP query parameter. const ( QueryKeyDepth = "depth" QueryKeyDir = "dir" QueryKeyFormat = "_format" QueryKeyLimit = "limit" QueryKeyPart = "_part" ) // Supported dir values. const ( DirBackward = "backward" DirForward = "forward" ) // Supported format values. const ( FormatDJSON = "djson" FormatHTML = "html" FormatJSON = "json" FormatNative = "native" FormatRaw = "raw" FormatText = "text" FormatZMK = "zmk" ) var formatEncoder = map[string]EncodingEnum{ FormatDJSON: EncoderDJSON, FormatHTML: EncoderHTML, FormatJSON: EncoderJSON, FormatNative: EncoderNative, FormatRaw: EncoderRaw, FormatText: EncoderText, FormatZMK: EncoderZmk, } var encoderFormat = map[EncodingEnum]string{} func init() { for k, v := range formatEncoder { encoderFormat[v] = k } } // Encoder returns the internal encoder code for the given format string. func Encoder(format string) EncodingEnum { if e, ok := formatEncoder[format]; ok { return e } return EncoderUnknown } // EncodingEnum lists all valid encoder keys. type EncodingEnum uint8 // Values for EncoderEnum const ( EncoderUnknown EncodingEnum = iota EncoderDJSON EncoderHTML EncoderJSON EncoderNative EncoderRaw EncoderText EncoderZmk ) // String representation of an encoder key. func (e EncodingEnum) String() string { if f, ok := encoderFormat[e]; ok { return f } return fmt.Sprintf("*Unknown*(%d)", e) } // Supported part values. const ( PartID = "id" PartMeta = "meta" PartContent = "content" PartZettel = "zettel" ) |
Added api/urlbuilder.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api contains common definition used for client and server. package api import ( "net/url" "strings" "zettelstore.de/z/domain/id" ) type urlQuery struct{ key, val string } // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { prefix string key byte path []string query []urlQuery fragment string } // NewURLBuilder creates a new URL builder with the given prefix and key. func NewURLBuilder(prefix string, key byte) *URLBuilder { return &URLBuilder{prefix: prefix, key: key} } // Clone an URLBuilder func (ub *URLBuilder) Clone() *URLBuilder { cpy := new(URLBuilder) cpy.key = ub.key if len(ub.path) > 0 { cpy.path = make([]string, 0, len(ub.path)) cpy.path = append(cpy.path, ub.path...) } if len(ub.query) > 0 { cpy.query = make([]urlQuery, 0, len(ub.query)) cpy.query = append(cpy.query, ub.query...) } cpy.fragment = ub.fragment return cpy } // SetZid sets the zettel identifier. func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder { if len(ub.path) > 0 { panic("Cannot add Zid") } ub.path = append(ub.path, zid.String()) return ub } // AppendPath adds a new path element func (ub *URLBuilder) AppendPath(p string) *URLBuilder { ub.path = append(ub.path, p) return ub } // AppendQuery adds a new query parameter func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder { ub.query = append(ub.query, urlQuery{key, value}) return ub } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() *URLBuilder { ub.query = nil ub.fragment = "" return ub } // SetFragment stores the fragment func (ub *URLBuilder) SetFragment(s string) *URLBuilder { ub.fragment = s return ub } // String produces a string value. func (ub *URLBuilder) String() string { var sb strings.Builder sb.WriteString(ub.prefix) if ub.key != '/' { sb.WriteByte(ub.key) } for _, p := range ub.path { sb.WriteByte('/') sb.WriteString(url.PathEscape(p)) } if len(ub.fragment) > 0 { sb.WriteByte('#') sb.WriteString(ub.fragment) } for i, q := range ub.query { if i == 0 { sb.WriteByte('?') } else { sb.WriteByte('&') } sb.WriteString(q.key) sb.WriteByte('=') sb.WriteString(url.QueryEscape(q.val)) } return sb.String() } |
Changes to ast/ast.go.
︙ | ︙ | |||
28 29 30 31 32 33 34 | Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. } // Node is the interface, all nodes must implement. type Node interface { | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. } // Node is the interface, all nodes must implement. type Node interface { WalkChildren(v Visitor) } // BlockNode is the interface that all block nodes must implement. type BlockNode interface { Node blockNode() } |
︙ | ︙ |
Changes to ast/attr_test.go.
1 | //----------------------------------------------------------------------------- | | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast_test import ( "testing" "zettelstore.de/z/ast" ) func TestHasDefault(t *testing.T) { t.Parallel() attr := &ast.Attributes{} if attr.HasDefault() { t.Error("Should not have default attr") } attr = &ast.Attributes{Attrs: map[string]string{"-": "value"}} if !attr.HasDefault() { t.Error("Should have default attr") } } func TestAttrClone(t *testing.T) { t.Parallel() orig := &ast.Attributes{} clone := orig.Clone() if len(clone.Attrs) > 0 { t.Error("Attrs must be empty") } orig = &ast.Attributes{Attrs: map[string]string{"": "0", "-": "1", "a": "b"}} |
︙ | ︙ |
Changes to ast/block.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | // ParaNode contains just a sequence of inline elements. // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } | | | | | | > > | | | | | | | | | | | | | | | | > > > | | | | > > | | | | | | | | | | | | > > > > | | > > > > > > > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | // ParaNode contains just a sequence of inline elements. // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } func (pn *ParaNode) blockNode() { /* Just a marker */ } func (pn *ParaNode) itemNode() { /* Just a marker */ } func (pn *ParaNode) descriptionNode() { /* Just a marker */ } // WalkChildren walks down the inline elements. func (pn *ParaNode) WalkChildren(v Visitor) { WalkInlineSlice(v, pn.Inlines) } //-------------------------------------------------------------------------- // VerbatimNode contains lines of uninterpreted text type VerbatimNode struct { Kind VerbatimKind Attrs *Attributes Lines []string } // VerbatimKind specifies the format that is applied to code inline nodes. type VerbatimKind uint8 // Constants for VerbatimCode const ( _ VerbatimKind = iota VerbatimProg // Program code. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown ) func (vn *VerbatimNode) blockNode() { /* Just a marker */ } func (vn *VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (vn *VerbatimNode) WalkChildren(v Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind Attrs *Attributes Blocks BlockSlice Inlines InlineSlice // Additional text at the end of the region } // RegionKind specifies the actual region type. type RegionKind uint8 // Values for RegionCode const ( _ RegionKind = iota RegionSpan // Just a span of blocks RegionQuote // A longer quotation RegionVerse // Line breaks matter ) func (rn *RegionNode) blockNode() { /* Just a marker */ } func (rn *RegionNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the blocks and the text. func (rn *RegionNode) WalkChildren(v Visitor) { WalkBlockSlice(v, rn.Blocks) WalkInlineSlice(v, rn.Inlines) } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { Level int Inlines InlineSlice // Heading text, possibly formatted Slug string // Heading text, suitable to be used as an URL fragment Attrs *Attributes } func (hn *HeadingNode) blockNode() { /* Just a marker */ } func (hn *HeadingNode) itemNode() { /* Just a marker */ } // WalkChildren walks the heading text. func (hn *HeadingNode) WalkChildren(v Visitor) { WalkInlineSlice(v, hn.Inlines) } //-------------------------------------------------------------------------- // HRuleNode specifies a horizontal rule. type HRuleNode struct { Attrs *Attributes } func (hn *HRuleNode) blockNode() { /* Just a marker */ } func (hn *HRuleNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (hn *HRuleNode) WalkChildren(v Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // NestedListNode specifies a nestable list, either ordered or unordered. type NestedListNode struct { Kind NestedListKind Items []ItemSlice Attrs *Attributes } // NestedListKind specifies the actual list type. type NestedListKind uint8 // Values for ListCode const ( _ NestedListKind = iota NestedListOrdered // Ordered list. NestedListUnordered // Unordered list. NestedListQuote // Quote list. ) func (ln *NestedListNode) blockNode() { /* Just a marker */ } func (ln *NestedListNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the items. func (ln *NestedListNode) WalkChildren(v Visitor) { for _, item := range ln.Items { WalkItemSlice(v, item) } } //-------------------------------------------------------------------------- // DescriptionListNode specifies a description list. type DescriptionListNode struct { Descriptions []Description } // Description is one element of a description list. type Description struct { Term InlineSlice Descriptions []DescriptionSlice } func (dn *DescriptionListNode) blockNode() {} // WalkChildren walks down to the descriptions. func (dn *DescriptionListNode) WalkChildren(v Visitor) { for _, desc := range dn.Descriptions { WalkInlineSlice(v, desc.Term) for _, dns := range desc.Descriptions { WalkDescriptionSlice(v, dns) } } } //-------------------------------------------------------------------------- // TableNode specifies a full table type TableNode struct { Header TableRow // The header row Align []Alignment // Default column alignment |
︙ | ︙ | |||
179 180 181 182 183 184 185 | _ Alignment = iota AlignDefault // Default alignment, inherited AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) | | | | > > > > > > > > > | | | | 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | _ Alignment = iota AlignDefault // Default alignment, inherited AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) func (tn *TableNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the cells. func (tn *TableNode) WalkChildren(v Visitor) { for _, cell := range tn.Header { WalkInlineSlice(v, cell.Inlines) } for _, row := range tn.Rows { for _, cell := range row { WalkInlineSlice(v, cell.Inlines) } } } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { Title string Syntax string Blob []byte } func (bn *BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (bn *BLOBNode) WalkChildren(v Visitor) { /* No children*/ } |
Changes to ast/inline.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | // Definitions of inline nodes. // TextNode just contains some text. type TextNode struct { Text string // The text itself. } | | | | | | | | | | | | | | | | > > | | | > > | | | > > | | | | | | > > | | | | | | | > > | | | | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | // Definitions of inline nodes. // TextNode just contains some text. type TextNode struct { Text string // The text itself. } func (tn *TextNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (tn *TextNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // TagNode contains a tag. type TagNode struct { Tag string // The text itself. } func (tn *TagNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (tn *TagNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // SpaceNode tracks inter-word space characters. type SpaceNode struct { Lexeme string } func (sn *SpaceNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (sn *SpaceNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // BreakNode signals a new line that must / should be interpreted as a new line break. type BreakNode struct { Hard bool // Hard line break? } func (bn *BreakNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (bn *BreakNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { Ref *Reference Inlines InlineSlice // The text associated with the link. OnlyRef bool // True if no text was specified. Attrs *Attributes // Optional attributes } func (ln *LinkNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the link text. func (ln *LinkNode) WalkChildren(v Visitor) { WalkInlineSlice(v, ln.Inlines) } // -------------------------------------------------------------------------- // ImageNode contains the specified image reference. type ImageNode struct { Ref *Reference // Reference to image Blob []byte // BLOB data of the image, as an alternative to Ref. Syntax string // Syntax of Blob Inlines InlineSlice // The text associated with the image. Attrs *Attributes // Optional attributes } func (in *ImageNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the image text. func (in *ImageNode) WalkChildren(v Visitor) { WalkInlineSlice(v, in.Inlines) } // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { Key string // The citation key Inlines InlineSlice // The text associated with the citation. Attrs *Attributes // Optional attributes } func (cn *CiteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the cite text. func (cn *CiteNode) WalkChildren(v Visitor) { WalkInlineSlice(v, cn.Inlines) } // -------------------------------------------------------------------------- // MarkNode contains the specified merked position. // It is a BlockNode too, because although it is typically parsed during inline // mode, it is moved into block mode afterwards. type MarkNode struct { Text string } func (mn *MarkNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (mn *MarkNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Inlines InlineSlice // The footnote text. Attrs *Attributes // Optional attributes } func (fn *FootnoteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the footnote text. func (fn *FootnoteNode) WalkChildren(v Visitor) { WalkInlineSlice(v, fn.Inlines) } // -------------------------------------------------------------------------- // FormatNode specifies some inline formatting. type FormatNode struct { Kind FormatKind Attrs *Attributes // Optional attributes. Inlines InlineSlice } // FormatKind specifies the format that is applied to the inline nodes. type FormatKind uint8 // Constants for FormatCode const ( _ FormatKind = iota FormatItalic // Italic text. FormatEmph // Semantically emphasized text. FormatBold // Bold text. FormatStrong // Semantically strongly emphasized text. FormatUnder // Underlined text. FormatInsert // Inserted text. FormatStrike // Text that is no longer relevant or no longer accurate. FormatDelete // Deleted text. FormatSuper // Superscripted text. FormatSub // SubscriptedText. FormatQuote // Quoted text. FormatQuotation // Quotation text. FormatSmall // Smaller text. FormatSpan // Generic inline container. FormatMonospace // Monospaced text. ) func (fn *FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. func (fn *FormatNode) WalkChildren(v Visitor) { WalkInlineSlice(v, fn.Inlines) } // -------------------------------------------------------------------------- // LiteralNode specifies some uninterpreted text. type LiteralNode struct { Kind LiteralKind Attrs *Attributes // Optional attributes. Text string } // LiteralKind specifies the format that is applied to code inline nodes. type LiteralKind uint8 // Constants for LiteralCode const ( _ LiteralKind = iota LiteralProg // Inline program code. LiteralKeyb // Keyboard strokes. LiteralOutput // Sample output. LiteralComment // Inline comment LiteralHTML // Inline HTML, e.g. for Markdown ) func (ln *LiteralNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (ln *LiteralNode) WalkChildren(v Visitor) { /* No children*/ } |
Changes to ast/ref_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "testing" "zettelstore.de/z/ast" ) func TestParseReference(t *testing.T) { testcases := []struct { link string err bool exp string }{ {"", true, ""}, {"123", false, "123"}, | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "testing" "zettelstore.de/z/ast" ) func TestParseReference(t *testing.T) { t.Parallel() testcases := []struct { link string err bool exp string }{ {"", true, ""}, {"123", false, "123"}, |
︙ | ︙ | |||
37 38 39 40 41 42 43 44 45 46 47 48 49 50 | if got.IsValid() && got.String() != tc.exp { t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got) } } } func TestReferenceIsZettelMaterial(t *testing.T) { testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, | > | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | if got.IsValid() && got.String() != tc.exp { t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got) } } } func TestReferenceIsZettelMaterial(t *testing.T) { t.Parallel() testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, |
︙ | ︙ |
Deleted ast/traverser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/visitor.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added ast/walk.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast // Visitor is a visitor for walking the AST. type Visitor interface { Visit(node Node) Visitor } // Walk traverses the AST. func Walk(v Visitor, node Node) { if v = v.Visit(node); v == nil { return } node.WalkChildren(v) v.Visit(nil) } // WalkBlockSlice traverse a block slice. func WalkBlockSlice(v Visitor, bns BlockSlice) { for _, bn := range bns { Walk(v, bn) } } // WalkInlineSlice traverses an inline slice. func WalkInlineSlice(v Visitor, ins InlineSlice) { for _, in := range ins { Walk(v, in) } } // WalkItemSlice traverses an item slice. func WalkItemSlice(v Visitor, ins ItemSlice) { for _, in := range ins { Walk(v, in) } } // WalkDescriptionSlice traverses an item slice. func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) { for _, dn := range dns { Walk(v, dn) } } |
Changes to auth/auth.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 | // Package auth provides services for authentification / authorization. package auth import ( "time" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | > < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // Package auth provides services for authentification / authorization. package auth import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. IsReadonly() bool |
︙ | ︙ | |||
75 76 77 78 79 80 81 | } // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | } // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) } // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. CanCreate(user, newMeta *meta.Meta) bool |
︙ | ︙ |
Changes to auth/impl/impl.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 | "io" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" | > < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "io" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) type myAuth struct { readonly bool owner id.Zid secret []byte |
︙ | ︙ | |||
175 176 177 178 179 180 181 | if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } | | | | 175 176 177 178 179 180 181 182 183 184 | if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } func (a *myAuth) BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { return policy.BoxWithPolicy(auth, a, unprotectedBox, rtConfig) } |
Added auth/policy/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" "zettelstore.de/z/web/server" ) // BoxWithPolicy wraps the given box inside a policy box. func BoxWithPolicy( auth server.Auth, manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig, ) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) return newBox(auth, box, pol), pol } // polBox implements a policy box. type polBox struct { auth server.Auth box box.Box policy auth.Policy } // newBox creates a new policy box. func newBox(auth server.Auth, box box.Box, policy auth.Policy) box.Box { return &polBox{ auth: auth, box: box, policy: policy, } } func (pp *polBox) Location() string { return pp.box.Location() } func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { user := pp.auth.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { zettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return domain.Zettel{}, err } user := pp.auth.GetUser(ctx) if pp.policy.CanRead(user, zettel.Meta) { return zettel, nil } return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } user := pp.auth.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { return pp.box.GetAllMeta(ctx, zid) } func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { return nil, box.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid) } func (pp *polBox) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { user := pp.auth.GetUser(ctx) canRead := pp.policy.CanRead s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.box.SelectMeta(ctx, s) } func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid user := pp.auth.GetUser(ctx) if !zid.IsValid() { return &box.ErrInvalidID{Zid: zid} } // Write existing zettel oldMeta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } if pp.policy.CanWrite(user, oldMeta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return pp.box.AllowRenameZettel(ctx, zid) } func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { meta, err := pp.box.GetMeta(ctx, curZid) if err != nil { return err } user := pp.auth.GetUser(ctx) if pp.policy.CanRename(user, meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.box.CanDeleteZettel(ctx, zid) } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { meta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } user := pp.auth.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } |
Changes to auth/policy/owner.go.
︙ | ︙ | |||
60 61 62 63 64 65 66 | if user == nil { return false } if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Only the user can read its own zettel return user.Zid == m.Zid } | > > | > > > > > | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | if user == nil { return false } if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Only the user can read its own zettel return user.Zid == m.Zid } switch o.manager.GetUserRole(user) { case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner: return true case meta.UserRoleCreator: return vis == meta.VisibilityCreator default: return false } } var noChangeUser = []string{ meta.KeyID, meta.KeyRole, meta.KeyUserID, meta.KeyUserRole, |
︙ | ︙ | |||
94 95 96 97 98 99 100 | for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } | | > | 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } switch userRole := o.manager.GetUserRole(user); userRole { case meta.UserRoleReader, meta.UserRoleCreator: return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { if user == nil || !o.pre.CanRename(user, m) { |
︙ | ︙ |
Deleted auth/policy/place.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to auth/policy/policy_test.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { testScene := []struct { readonly bool withAuth bool expert bool }{ {true, true, true}, {true, true, false}, | > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { t.Parallel() testScene := []struct { readonly bool withAuth bool expert bool }{ {true, true, true}, {true, true, false}, |
︙ | ︙ | |||
84 85 86 87 88 89 90 | type authConfig struct{ expert bool } func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(meta.KeyVisibility); ok { | < < < < | < < < > > > > > > > > > > > > > > > > > > > > > > | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | type authConfig struct{ expert bool } func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(meta.KeyVisibility); ok { return meta.GetVisibility(vis) } return meta.VisibilityLogin } func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly, isExpert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() userZettel := newUserZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {creator, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Ordinary zettel {anonUser, zettel, !withAuth && !readonly}, {creator, zettel, !readonly}, {reader, zettel, !withAuth && !readonly}, {writer, zettel, !readonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // User zettel {anonUser, userZettel, !withAuth && !readonly}, {creator, userZettel, !withAuth && !readonly}, {reader, userZettel, !withAuth && !readonly}, {writer, userZettel, !withAuth && !readonly}, {owner, userZettel, !readonly}, {owner2, userZettel, !readonly}, } for _, tc := range testCases { t.Run("Create", func(tt *testing.T) { got := pol.CanCreate(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRead(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() creatorZettel := newCreatorZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {creator, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Ordinary zettel {anonUser, zettel, !withAuth}, {creator, zettel, !withAuth}, {reader, zettel, true}, {writer, zettel, true}, {owner, zettel, true}, {owner2, zettel, true}, // Public zettel {anonUser, publicZettel, true}, {creator, publicZettel, true}, {reader, publicZettel, true}, {writer, publicZettel, true}, {owner, publicZettel, true}, {owner2, publicZettel, true}, // Creator zettel {anonUser, creatorZettel, !withAuth}, {creator, creatorZettel, true}, {reader, creatorZettel, true}, {writer, creatorZettel, true}, {owner, creatorZettel, true}, {owner2, creatorZettel, true}, // Login zettel {anonUser, loginZettel, !withAuth}, {creator, loginZettel, !withAuth}, {reader, loginZettel, true}, {writer, loginZettel, true}, {owner, loginZettel, true}, {owner2, loginZettel, true}, // Owner zettel {anonUser, ownerZettel, !withAuth}, {creator, ownerZettel, !withAuth}, {reader, ownerZettel, !withAuth}, {writer, ownerZettel, !withAuth}, {owner, ownerZettel, true}, {owner2, ownerZettel, true}, // Expert zettel {anonUser, expertZettel, !withAuth && expert}, {creator, expertZettel, !withAuth && expert}, {reader, expertZettel, !withAuth && expert}, {writer, expertZettel, !withAuth && expert}, {owner, expertZettel, expert}, {owner2, expertZettel, expert}, // Other user zettel {anonUser, userZettel, !withAuth}, {creator, userZettel, !withAuth}, {reader, userZettel, !withAuth}, {writer, userZettel, !withAuth}, {owner, userZettel, true}, {owner2, userZettel, true}, // Own user zettel {creator, creator, true}, {reader, reader, true}, {writer, writer, true}, {owner, owner, true}, {owner, owner2, true}, {owner2, owner, true}, {owner2, owner2, true}, } for _, tc := range testCases { t.Run("Read", func(tt *testing.T) { got := pol.CanRead(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testWrite(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() |
︙ | ︙ | |||
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | user *meta.Meta old *meta.Meta new *meta.Meta exp bool }{ // No old and new meta {anonUser, nil, nil, false}, {reader, nil, nil, false}, {writer, nil, nil, false}, {owner, nil, nil, false}, {owner2, nil, nil, false}, // No old meta {anonUser, nil, zettel, false}, {reader, nil, zettel, false}, {writer, nil, zettel, false}, {owner, nil, zettel, false}, {owner2, nil, zettel, false}, // No new meta {anonUser, zettel, nil, false}, {reader, zettel, nil, false}, {writer, zettel, nil, false}, {owner, zettel, nil, false}, {owner2, zettel, nil, false}, // Old an new zettel have different zettel identifier {anonUser, zettel, publicZettel, false}, {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel {anonUser, zettel, zettel, notAuthNotReadonly}, {reader, zettel, zettel, notAuthNotReadonly}, {writer, zettel, zettel, !readonly}, {owner, zettel, zettel, !readonly}, {owner2, zettel, zettel, !readonly}, // Public zettel {anonUser, publicZettel, publicZettel, notAuthNotReadonly}, {reader, publicZettel, publicZettel, notAuthNotReadonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel {anonUser, loginZettel, loginZettel, notAuthNotReadonly}, {reader, loginZettel, loginZettel, notAuthNotReadonly}, {writer, loginZettel, loginZettel, !readonly}, {owner, loginZettel, loginZettel, !readonly}, {owner2, loginZettel, loginZettel, !readonly}, // Owner zettel {anonUser, ownerZettel, ownerZettel, notAuthNotReadonly}, {reader, ownerZettel, ownerZettel, notAuthNotReadonly}, {writer, ownerZettel, ownerZettel, notAuthNotReadonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel {anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, expertZettel, !readonly && expert}, {owner2, expertZettel, expertZettel, !readonly && expert}, // Other user zettel {anonUser, userZettel, userZettel, notAuthNotReadonly}, {reader, userZettel, userZettel, notAuthNotReadonly}, {writer, userZettel, userZettel, notAuthNotReadonly}, {owner, userZettel, userZettel, !readonly}, {owner2, userZettel, userZettel, !readonly}, // Own user zettel {reader, reader, reader, !readonly}, {writer, writer, writer, !readonly}, {owner, owner, owner, !readonly}, {owner2, owner2, owner2, !readonly}, // Writer cannot change importand metadata of its own user zettel {writer, writer, writerNew, notAuthNotReadonly}, // No r/o zettel {anonUser, roFalse, roFalse, notAuthNotReadonly}, {reader, roFalse, roFalse, notAuthNotReadonly}, {writer, roFalse, roFalse, !readonly}, {owner, roFalse, roFalse, !readonly}, {owner2, roFalse, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, roReader, false}, {reader, roReader, roReader, false}, {writer, roReader, roReader, !readonly}, {owner, roReader, roReader, !readonly}, {owner2, roReader, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, roWriter, false}, {reader, roWriter, roWriter, false}, {writer, roWriter, roWriter, false}, {owner, roWriter, roWriter, !readonly}, {owner2, roWriter, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, roOwner, false}, {reader, roOwner, roOwner, false}, {writer, roOwner, roOwner, false}, {owner, roOwner, roOwner, false}, {owner2, roOwner, roOwner, false}, // r/o = true zettel {anonUser, roTrue, roTrue, false}, {reader, roTrue, roTrue, false}, {writer, roTrue, roTrue, false}, {owner, roTrue, roTrue, false}, {owner2, roTrue, roTrue, false}, } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, notAuthNotReadonly}, {reader, zettel, notAuthNotReadonly}, {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // No r/o zettel {anonUser, roFalse, notAuthNotReadonly}, {reader, roFalse, notAuthNotReadonly}, {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, {owner2, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, false}, {reader, roOwner, false}, {writer, roOwner, false}, {owner, roOwner, false}, {owner2, roOwner, false}, // r/o = true zettel {anonUser, roTrue, false}, {reader, roTrue, false}, {writer, roTrue, false}, {owner, roTrue, false}, {owner2, roTrue, false}, } for _, tc := range testCases { t.Run("Rename", func(tt *testing.T) { got := pol.CanRename(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, notAuthNotReadonly}, {reader, zettel, notAuthNotReadonly}, {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // No r/o zettel {anonUser, roFalse, notAuthNotReadonly}, {reader, roFalse, notAuthNotReadonly}, {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, {owner2, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, false}, {reader, roOwner, false}, {writer, roOwner, false}, {owner, roOwner, false}, {owner2, roOwner, false}, // r/o = true zettel {anonUser, roTrue, false}, {reader, roTrue, false}, {writer, roTrue, false}, {owner, roTrue, false}, {owner2, roTrue, false}, } for _, tc := range testCases { t.Run("Delete", func(tt *testing.T) { got := pol.CanDelete(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } const ( | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | > | | | | > > > > > > > | 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 | user *meta.Meta old *meta.Meta new *meta.Meta exp bool }{ // No old and new meta {anonUser, nil, nil, false}, {creator, nil, nil, false}, {reader, nil, nil, false}, {writer, nil, nil, false}, {owner, nil, nil, false}, {owner2, nil, nil, false}, // No old meta {anonUser, nil, zettel, false}, {creator, nil, zettel, false}, {reader, nil, zettel, false}, {writer, nil, zettel, false}, {owner, nil, zettel, false}, {owner2, nil, zettel, false}, // No new meta {anonUser, zettel, nil, false}, {creator, zettel, nil, false}, {reader, zettel, nil, false}, {writer, zettel, nil, false}, {owner, zettel, nil, false}, {owner2, zettel, nil, false}, // Old an new zettel have different zettel identifier {anonUser, zettel, publicZettel, false}, {creator, zettel, publicZettel, false}, {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel {anonUser, zettel, zettel, notAuthNotReadonly}, {creator, zettel, zettel, notAuthNotReadonly}, {reader, zettel, zettel, notAuthNotReadonly}, {writer, zettel, zettel, !readonly}, {owner, zettel, zettel, !readonly}, {owner2, zettel, zettel, !readonly}, // Public zettel {anonUser, publicZettel, publicZettel, notAuthNotReadonly}, {creator, publicZettel, publicZettel, notAuthNotReadonly}, {reader, publicZettel, publicZettel, notAuthNotReadonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel {anonUser, loginZettel, loginZettel, notAuthNotReadonly}, {creator, loginZettel, loginZettel, notAuthNotReadonly}, {reader, loginZettel, loginZettel, notAuthNotReadonly}, {writer, loginZettel, loginZettel, !readonly}, {owner, loginZettel, loginZettel, !readonly}, {owner2, loginZettel, loginZettel, !readonly}, // Owner zettel {anonUser, ownerZettel, ownerZettel, notAuthNotReadonly}, {creator, ownerZettel, ownerZettel, notAuthNotReadonly}, {reader, ownerZettel, ownerZettel, notAuthNotReadonly}, {writer, ownerZettel, ownerZettel, notAuthNotReadonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel {anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert}, {creator, expertZettel, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, expertZettel, !readonly && expert}, {owner2, expertZettel, expertZettel, !readonly && expert}, // Other user zettel {anonUser, userZettel, userZettel, notAuthNotReadonly}, {creator, userZettel, userZettel, notAuthNotReadonly}, {reader, userZettel, userZettel, notAuthNotReadonly}, {writer, userZettel, userZettel, notAuthNotReadonly}, {owner, userZettel, userZettel, !readonly}, {owner2, userZettel, userZettel, !readonly}, // Own user zettel {creator, creator, creator, !readonly}, {reader, reader, reader, !readonly}, {writer, writer, writer, !readonly}, {owner, owner, owner, !readonly}, {owner2, owner2, owner2, !readonly}, // Writer cannot change importand metadata of its own user zettel {writer, writer, writerNew, notAuthNotReadonly}, // No r/o zettel {anonUser, roFalse, roFalse, notAuthNotReadonly}, {creator, roFalse, roFalse, notAuthNotReadonly}, {reader, roFalse, roFalse, notAuthNotReadonly}, {writer, roFalse, roFalse, !readonly}, {owner, roFalse, roFalse, !readonly}, {owner2, roFalse, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, roReader, false}, {creator, roReader, roReader, false}, {reader, roReader, roReader, false}, {writer, roReader, roReader, !readonly}, {owner, roReader, roReader, !readonly}, {owner2, roReader, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, roWriter, false}, {creator, roWriter, roWriter, false}, {reader, roWriter, roWriter, false}, {writer, roWriter, roWriter, false}, {owner, roWriter, roWriter, !readonly}, {owner2, roWriter, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, roOwner, false}, {creator, roOwner, roOwner, false}, {reader, roOwner, roOwner, false}, {writer, roOwner, roOwner, false}, {owner, roOwner, roOwner, false}, {owner2, roOwner, roOwner, false}, // r/o = true zettel {anonUser, roTrue, roTrue, false}, {creator, roTrue, roTrue, false}, {reader, roTrue, roTrue, false}, {writer, roTrue, roTrue, false}, {owner, roTrue, roTrue, false}, {owner2, roTrue, roTrue, false}, } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {creator, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, notAuthNotReadonly}, {creator, zettel, notAuthNotReadonly}, {reader, zettel, notAuthNotReadonly}, {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, notAuthNotReadonly && expert}, {creator, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // No r/o zettel {anonUser, roFalse, notAuthNotReadonly}, {creator, roFalse, notAuthNotReadonly}, {reader, roFalse, notAuthNotReadonly}, {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {creator, roReader, false}, {reader, roReader, false}, {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {creator, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, {owner2, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, false}, {creator, roOwner, false}, {reader, roOwner, false}, {writer, roOwner, false}, {owner, roOwner, false}, {owner2, roOwner, false}, // r/o = true zettel {anonUser, roTrue, false}, {creator, roTrue, false}, {reader, roTrue, false}, {writer, roTrue, false}, {owner, roTrue, false}, {owner2, roTrue, false}, } for _, tc := range testCases { t.Run("Rename", func(tt *testing.T) { got := pol.CanRename(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {creator, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, notAuthNotReadonly}, {creator, zettel, notAuthNotReadonly}, {reader, zettel, notAuthNotReadonly}, {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, notAuthNotReadonly && expert}, {creator, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // No r/o zettel {anonUser, roFalse, notAuthNotReadonly}, {creator, roFalse, notAuthNotReadonly}, {reader, roFalse, notAuthNotReadonly}, {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {creator, roReader, false}, {reader, roReader, false}, {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {creator, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, {owner2, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, false}, {creator, roOwner, false}, {reader, roOwner, false}, {writer, roOwner, false}, {owner, roOwner, false}, {owner2, roOwner, false}, // r/o = true zettel {anonUser, roTrue, false}, {creator, roTrue, false}, {reader, roTrue, false}, {writer, roTrue, false}, {owner, roTrue, false}, {owner2, roTrue, false}, } for _, tc := range testCases { t.Run("Delete", func(tt *testing.T) { got := pol.CanDelete(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } const ( creatorZid = id.Zid(1013) readerZid = id.Zid(1013) writerZid = id.Zid(1015) ownerZid = id.Zid(1017) owner2Zid = id.Zid(1019) zettelZid = id.Zid(1021) visZid = id.Zid(1023) userZid = id.Zid(1025) ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) user.Set(meta.KeyTitle, "Creator") user.Set(meta.KeyRole, meta.ValueRoleUser) user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator) return user } func newReader() *meta.Meta { user := meta.New(readerZid) user.Set(meta.KeyTitle, "Reader") user.Set(meta.KeyRole, meta.ValueRoleUser) user.Set(meta.KeyUserRole, meta.ValueUserRoleReader) return user } |
︙ | ︙ | |||
559 560 561 562 563 564 565 566 567 568 569 570 571 572 | return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } | > > > > > > | 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newCreatorZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Creator Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } |
︙ | ︙ |
Added box/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box import ( "context" "errors" "fmt" "io" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. // Format is dependent of the box. Location() string // CanCreateZettel returns true, if box could possibly create a new zettel. CanCreateZettel(ctx context.Context) bool // CreateZettel creates a new zettel. // Returns the new zettel id (and an error indication). CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (id.Set, error) // CanUpdateZettel returns true, if box could possibly update the given zettel. CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel domain.Zettel) error // AllowRenameZettel returns true, if box will not disallow renaming the zettel. AllowRenameZettel(ctx context.Context, zid id.Zid) bool // RenameZettel changes the current Zid to a new Zid. RenameZettel(ctx context.Context, curZid, newZid id.Zid) error // CanDeleteZettel returns true, if box could possibly delete the given zettel. CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) // ReadStats populates st with box statistics ReadStats(st *ManagedBoxStats) } // ManagedBoxStats records statistics about the box. type ManagedBoxStats struct { // ReadOnly indicates that the content of a box cannot change. ReadOnly bool // Zettel is the number of zettel managed by the box. Zettel int } // StartStopper performs simple lifecycle management. type StartStopper interface { // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. Start(ctx context.Context) error // Stop the started box. Now only the Start() function is allowed. Stop(ctx context.Context) error } // Box is to be used outside the box package and its descendants. type Box interface { BaseBox // SelectMeta returns a list of metadata that comply to the given selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) // GetAllZettel retrieves a specific zettel from all managed boxes. GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) } // Stats record stattistics about a box. type Stats struct { // ReadOnly indicates that boxes cannot be modified. ReadOnly bool // NumManagedBoxes is the number of boxes managed. NumManagedBoxes int // Zettel is the number of zettel managed by the box, including // duplicates across managed boxes. ZettelTotal int // LastReload stores the timestamp when a full re-index was done. LastReload time.Time // DurLastReload is the duration of the last full re-index run. DurLastReload time.Duration // IndexesSinceReload counts indexing a zettel since the full re-index. IndexesSinceReload uint64 // ZettelIndexed is the number of zettel managed by the indexer. ZettelIndexed int // IndexUpdates count the number of metadata updates. IndexUpdates uint64 // IndexedWords count the different words indexed. IndexedWords uint64 // IndexedUrls count the different URLs indexed. IndexedUrls uint64 } // Manager is a box-managing box. type Manager interface { Box StartStopper Subject // ReadStats populates st with box statistics ReadStats(st *Stats) // Dump internal data to a Writer. Dump(w io.Writer) } // UpdateReason gives an indication, why the ObserverFunc was called. type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota OnReload // Box was reloaded OnUpdate // A zettel was created or changed OnDelete // A zettel was removed ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box Box Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. type UpdateFunc func(UpdateInfo) // Subject is a box that notifies observers about changes. type Subject interface { // RegisterObserver registers an observer that will be notified // if one or all zettel are found to be changed. RegisterObserver(UpdateFunc) } // Enricher is used to update metadata by adding new properties. type Enricher interface { // Enrich computes additional properties and updates the given metadata. // It is typically called by zettel reading methods. Enrich(ctx context.Context, m *meta.Meta, boxNumber int) } // NoEnrichContext will signal an enricher that nothing has to be done. // This is useful for an Indexer, but also for some box.Box calls, when // just the plain metadata is needed. func NoEnrichContext(ctx context.Context) context.Context { return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey) } type ctxNoEnrichType struct{} var ctxNoEnrichKey ctxNoEnrichType // DoNotEnrich determines if the context is marked to not enrich metadata. func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid } // NewErrNotAllowed creates an new authorization error. func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error { return &ErrNotAllowed{ Op: op, User: user, Zid: zid, } } func (err *ErrNotAllowed) Error() string { if err.User == nil { if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for not authorized user", err.Op, err.Zid.String()) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", err.Op, err.Zid.String(), err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } // Is return true, if the error is of type ErrNotAllowed. func (err *ErrNotAllowed) Is(target error) bool { return true } // ErrStarted is returned when trying to start an already started box. var ErrStarted = errors.New("box is already started") // ErrStopped is returned if calling methods on a box that was not started. var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") // ErrNotFound is returned if a zettel was not found in the box. var ErrNotFound = errors.New("zettel not found") // ErrConflict is returned if a box operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") // ErrInvalidID is returned if the zettel id is not appropriate for the box operation. type ErrInvalidID struct{ Zid id.Zid } func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } |
Added box/compbox/compbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) func init() { manager.Register( " comp", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher), nil }) } type compBox struct { number int enricher box.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta content func(*meta.Meta) string }{ id.VersionZid: {genVersionBuildM, genVersionBuildC}, id.HostZid: {genVersionHostM, genVersionHostC}, id.OperatingSystemZid: {genVersionOSM, genVersionOSC}, id.BoxManagerZid: {genManagerM, genManagerC}, id.MetadataKeyZid: {genKeysM, genKeysC}, id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox { return &compBox{number: boxNumber, enricher: mf} } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (pp *compBox) Location() string { return "" } func (pp *compBox) CanCreateZettel(ctx context.Context) bool { return false } func (pp *compBox) CreateZettel( ctx context.Context, zettel domain.Zettel) (id.Zid, error) { return id.Invalid, box.ErrReadOnly } func (pp *compBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { return domain.Zettel{ Meta: m, Content: domain.NewContent(genContent(m)), }, nil } return domain.Zettel{Meta: m}, nil } } return domain.Zettel{}, box.ErrNotFound } func (pp *compBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { if gen, ok := myZettel[zid]; ok { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) return m, nil } } } return nil, box.ErrNotFound } func (pp *compBox) FetchZids(ctx context.Context) (id.Set, error) { result := id.NewSetCap(len(myZettel)) for zid, gen := range myZettel { if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { result[zid] = true } } } return result, nil } func (pp *compBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { for zid, gen := range myZettel { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) pp.enricher.Enrich(ctx, m, pp.number) if match(m) { res = append(res, m) } } } } return res, nil } func (pp *compBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return false } func (pp *compBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return box.ErrReadOnly } func (pp *compBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } func (pp *compBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := myZettel[curZid]; ok { return box.ErrReadOnly } return box.ErrNotFound } func (pp *compBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } func (pp *compBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := myZettel[zid]; ok { return box.ErrReadOnly } return box.ErrNotFound } func (pp *compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) } func updateMeta(m *meta.Meta) { m.Set(meta.KeyNoIndex, meta.ValueTrue) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) m.Set(meta.KeyRole, meta.ValueRoleConfiguration) m.Set(meta.KeyLang, meta.ValueLangEN) m.Set(meta.KeyReadOnly, meta.ValueTrue) if _, ok := m.Get(meta.KeyVisibility); !ok { m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } |
Added box/compbox/config.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Startup Configuration") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func genConfigZettelC(m *meta.Meta) string { var sb strings.Builder for i, p := range myConfig.Pairs(false) { if i > 0 { sb.WriteByte('\n') } sb.WriteString("; ''") sb.WriteString(p.Key) sb.WriteString("''") if p.Value != "" { sb.WriteString("\n: ``") for _, r := range p.Value { if r == '`' { sb.WriteByte('\\') } sb.WriteRune(r) } sb.WriteString("``") } } return sb.String() } |
Added box/compbox/keys.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genKeysC(*meta.Meta) string { keys := meta.GetSortedKeyDescriptions() var sb strings.Builder sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&sb, "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return sb.String() } |
Added box/compbox/manager.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Box Manager") return m } func genManagerC(*meta.Meta) string { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) if len(kvl) == 0 { return "No statistics available" } var sb strings.Builder sb.WriteString("|=Name|=Value>\n") for _, kv := range kvl { fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value) } return sb.String() } |
Added box/compbox/version.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "fmt" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, title) m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func genVersionBuildC(*meta.Meta) string { return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string) } func genVersionHostM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Host") } func genVersionHostC(*meta.Meta) string { return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string) } func genVersionOSM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Operating System") } func genVersionOSC(*meta.Meta) string { return fmt.Sprintf( "%v/%v", kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string), kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string), ) } |
Added box/constbox/base.css.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; font-family: serif; scroll-behavior: smooth; height: 100%; } body { margin: 0; min-height: 100vh; text-rendering: optimizeSpeed; line-height: 1.4; overflow-x: hidden; background-color: #f8f8f8 ; height: 100%; } nav.zs-menu { background-color: hsl(210, 28%, 90%); overflow: auto; white-space: nowrap; font-family: sans-serif; padding-left: .5rem; } nav.zs-menu > a { float:left; display: block; text-align: center; padding:.41rem .5rem; text-decoration: none; color:black; } nav.zs-menu > a:hover, .zs-dropdown:hover button { background-color: hsl(210, 28%, 80%); } nav.zs-menu form { float: right; } nav.zs-menu form input[type=text] { padding: .12rem; border: none; margin-top: .25rem; margin-right: .5rem; } .zs-dropdown { float: left; overflow: hidden; } .zs-dropdown > button { font-size: 16px; border: none; outline: none; color: black; padding:.41rem .5rem; background-color: inherit; font-family: inherit; margin: 0; } .zs-dropdown-content { display: none; position: absolute; background-color: #f9f9f9; min-width: 160px; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 1; } .zs-dropdown-content > a { float: none; color: black; padding:.41rem .5rem; text-decoration: none; display: block; text-align: left; } .zs-dropdown-content > a:hover { background-color: hsl(210, 28%, 75%); } .zs-dropdown:hover > .zs-dropdown-content { display: block; } main { padding: 0 1rem; } article > * + * { margin-top: .5rem; } article header { padding: 0; margin: 0; } h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal } h1 { font-size:1.5rem; margin:.65rem 0 } h2 { font-size:1.25rem; margin:.70rem 0 } h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0; } ol,ul { padding-left: 1.1rem; } li,figure,figcaption,dl { margin: 0; } dt { margin: .5rem 0 0 0; } dt+dd { margin-top: 0; } dd { margin: .5rem 0 0 2rem; } dd > p:first-child { margin: 0 0 0 0; } blockquote { border-left: 0.5rem solid lightgray; padding-left: 1rem; margin-left: 1rem; margin-right: 2rem; font-style: italic; } blockquote p { margin-bottom: .5rem; } blockquote cite { font-style: normal; } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } th,td { text-align: left; padding: .25rem .5rem; } td { border-bottom: 1px solid hsl(0, 0%, 85%); } thead th { border-bottom: 2px solid hsl(0, 0%, 70%); } tfoot th { border-top: 2px solid hsl(0, 0%, 70%); } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } main form div { margin: .5em 0 0 0 } input { font-family: monospace; } input[type="submit"],button,select { font: inherit; } label { font-family: sans-serif; font-size:.9rem } label::after { content:":" } textarea { font-family: monospace; resize: vertical; width: 100%; } .zs-input { padding: .5em; display:block; border:none; border-bottom:1px solid #ccc; width:100%; } .zs-button { float:right; margin: .5em 0 .5em 1em; } a:not([class]) { text-decoration-skip-ink: auto; } .zs-broken { text-decoration: line-through; } img { max-width: 100%; } .zs-endnotes { padding-top: .5rem; border-top: 1px solid; } code,pre,kbd { font-family: monospace; font-size: 85%; } code { padding: .1rem .2rem; background: #f0f0f0; border: 1px solid #ccc; border-radius: .25rem; } pre { padding: .5rem .7rem; max-width: 100%; overflow: auto; border: 1px solid #ccc; border-radius: .5rem; background: #f0f0f0; } pre code { font-size: 95%; position: relative; padding: 0; border: none; } div.zs-indication { padding: .5rem .7rem; max-width: 100%; border-radius: .5rem; border: 1px solid black; } div.zs-indication p:first-child { margin-top: 0; } span.zs-indication { border: 1px solid black; border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } .zs-example { border-style: dotted !important } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } kbd { background: hsl(210, 5%, 100%); border: 1px solid hsl(210, 5%, 70%); border-radius: .25rem; padding: .1rem .2rem; font-size: 75%; } .zs-meta { font-size:.75rem; color:#444; margin-bottom:1rem; } .zs-meta a { color:#444; } h1+.zs-meta { margin-top:-1rem; } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } details > ul { margin-top:0; padding-left:2rem; background-color: #eee; } footer { padding: 0 1rem; } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } |
Added box/constbox/base.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="referrer" content="no-referrer"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> <meta name="format-detection" content="telephone=no"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{CSSBaseURL}}}"> <link rel="stylesheet" href="{{{CSSUserURL}}}"> <title>{{Title}}</title> </head> <body> <nav class="zs-menu"> <a href="{{{HomeURL}}}">Home</a> {{#WithUser}} <div class="zs-dropdown"> <button>User</button> <nav class="zs-dropdown-content"> {{#WithAuth}} {{#UserIsValid}} <a href="{{{UserZettelURL}}}">{{UserIdent}}</a> {{/UserIsValid}} {{^UserIsValid}} <a href="{{{LoginURL}}}">Login</a> {{/UserIsValid}} {{#UserIsValid}} <a href="{{{UserLogoutURL}}}">Logout</a> {{/UserIsValid}} {{/WithAuth}} </nav> </div> {{/WithUser}} <div class="zs-dropdown"> <button>Lists</button> <nav class="zs-dropdown-content"> <a href="{{{ListZettelURL}}}">List Zettel</a> <a href="{{{ListRolesURL}}}">List Roles</a> <a href="{{{ListTagsURL}}}">List Tags</a> </nav> </div> {{#HasNewZettelLinks}} <div class="zs-dropdown"> <button>New</button> <nav class="zs-dropdown-content"> {{#NewZettelLinks}} <a href="{{{URL}}}">{{Text}}</a> {{/NewZettelLinks}} </nav> </div> {{/HasNewZettelLinks}} <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="s"> </form> </nav> <main class="content"> {{{Content}}} </main> {{#FooterHTML}} <footer> {{{FooterHTML}}} </footer> {{/FooterHTML}} </body> </html> |
Added box/constbox/constbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ number: cdata.Number, zettel: constZettelMap, enricher: cdata.Enricher, }, nil }) } type constHeader map[string]string func makeMeta(zid id.Zid, h constHeader) *meta.Meta { m := meta.New(zid) for k, v := range h { m.Set(k, v) } return m } type constZettel struct { header constHeader content domain.Content } type constBox struct { number int zettel map[id.Zid]constZettel enricher box.Enricher } func (cp *constBox) Location() string { return "const:" } func (cp *constBox) CanCreateZettel(ctx context.Context) bool { return false } func (cp *constBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { return id.Invalid, box.ErrReadOnly } func (cp *constBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { if z, ok := cp.zettel[zid]; ok { return domain.Zettel{Meta: makeMeta(zid, z.header), Content: z.content}, nil } return domain.Zettel{}, box.ErrNotFound } func (cp *constBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { if z, ok := cp.zettel[zid]; ok { return makeMeta(zid, z.header), nil } return nil, box.ErrNotFound } func (cp *constBox) FetchZids(ctx context.Context) (id.Set, error) { result := id.NewSetCap(len(cp.zettel)) for zid := range cp.zettel { result[zid] = true } return result, nil } func (cp *constBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { for zid, zettel := range cp.zettel { m := makeMeta(zid, zettel.header) cp.enricher.Enrich(ctx, m, cp.number) if match(m) { res = append(res, m) } } return res, nil } func (cp *constBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return false } func (cp *constBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return box.ErrReadOnly } func (cp *constBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := cp.zettel[zid] return !ok } func (cp *constBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := cp.zettel[curZid]; ok { return box.ErrReadOnly } return box.ErrNotFound } func (cp *constBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } func (cp *constBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := cp.zettel[zid]; ok { return box.ErrReadOnly } return box.ErrNotFound } func (cp *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cp.zettel) } const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityOwner, }, domain.NewContent("")}, id.LicenseZid: { constHeader{ meta.KeyTitle: "Zettelstore License", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxText, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentLicense)}, id.AuthorsZid: { constHeader{ meta.KeyTitle: "Zettelstore Contributors", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentContributors)}, id.DependenciesZid: { constHeader{ meta.KeyTitle: "Zettelstore Dependencies", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Login Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Info HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentInfoMustache)}, id.ContextTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Context HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Rename Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Delete HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore List Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentListZettelMustache)}, id.RolesTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore List Roles HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentListRolesMustache)}, id.TagsTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore List Tags HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentListTagsMustache)}, id.ErrorTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Error HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentErrorMustache)}, id.BaseCSSZid: { constHeader{ meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: "css", meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentBaseCSS)}, id.UserCSSZid: { constHeader{ meta.KeyTitle: "Zettelstore User CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: "css", meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent("/* User-defined CSS */")}, id.EmojiZid: { constHeader{ meta.KeyTitle: "Generic Emoji", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxGif, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ meta.KeyTitle: "New Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityCreator, }, domain.NewContent(contentNewTOCZettel)}, id.TemplateNewZettelZid: { constHeader{ meta.KeyTitle: "New Zettel", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyVisibility: meta.ValueVisibilityCreator, }, domain.NewContent("")}, id.TemplateNewUserZid: { constHeader{ meta.KeyTitle: "New User", meta.KeyRole: meta.ValueRoleUser, meta.KeySyntax: meta.ValueSyntaxNone, meta.NewPrefix + meta.KeyCredential: "", meta.NewPrefix + meta.KeyUserID: "", meta.NewPrefix + meta.KeyUserRole: meta.ValueUserRoleReader, meta.KeyVisibility: meta.ValueVisibilityOwner, }, domain.NewContent("")}, id.DefaultHomeZid: { constHeader{ meta.KeyTitle: "Home", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, }, domain.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense string //go:embed contributors.zettel var contentContributors string //go:embed dependencies.zettel var contentDependencies string //go:embed base.mustache var contentBaseMustache string //go:embed login.mustache var contentLoginMustache string //go:embed zettel.mustache var contentZettelMustache string //go:embed info.mustache var contentInfoMustache string //go:embed context.mustache var contentContextMustache string //go:embed form.mustache var contentFormMustache string //go:embed rename.mustache var contentRenameMustache string //go:embed delete.mustache var contentDeleteMustache string //go:embed listzettel.mustache var contentListZettelMustache string //go:embed listroles.mustache var contentListRolesMustache string //go:embed listtags.mustache var contentListTagsMustache string //go:embed error.mustache var contentErrorMustache string //go:embed base.css var contentBaseCSS string //go:embed emoji_spin.gif var contentEmoji string //go:embed newtoc.zettel var contentNewTOCZettel string //go:embed home.zettel var contentHomeZettel string |
Added box/constbox/context.mustache.
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <nav> <header> <h1>{{Title}}</h1> <div class="zs-meta"> <a href="{{{InfoURL}}}">Info</a> · <a href="?dir=backward">Backward</a> · <a href="?dir=both">Both</a> · <a href="?dir=forward">Forward</a> · Depth:{{#Depths}} <a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}} </div> </header> <p><a href="{{{Start.URL}}}">{{{Start.Text}}}</a></p> <ul> {{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li> {{/Metas}}</ul> </nav> |
Added box/constbox/contributors.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | Zettelstore is a software for humans made from humans. === Licensor(s) * Detlef Stern [[mailto:ds@zettelstore.de]] ** Main author ** Maintainer === Contributors |
Added box/constbox/delete.mustache.
> > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <article> <header> <h1>Delete Zettel {{Zid}}</h1> </header> <p>Do you really want to delete this zettel?</p> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> <form method="POST"> <input class="zs-button" type="submit" value="Delete"> </form> </article> {{end}} |
Added box/constbox/dependencies.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | Zettelstore is made with the help of other software and other artifacts. Thank you very much! This zettel lists all of them, together with their license. === Go runtime and associated libraries ; License : BSD 3-Clause "New" or "Revised" License ``` Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === Fsnotify ; URL : [[https://fsnotify.org/]] ; License : BSD 3-Clause "New" or "Revised" License ; Source : [[https://github.com/fsnotify/fsnotify]] ``` Copyright (c) 2012 The Go Authors. All rights reserved. Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === hoisie/mustache / cbroglie/mustache ; URL & Source : [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] ; License : MIT License ; Remarks : cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]). cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache. cbroglie/mustache obviously continues with the original license. ``` Copyright (c) 2009 Michael Hoisie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === pascaldekloe/jwt ; URL & Source : [[https://github.com/pascaldekloe/jwt]] ; License : [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]] ``` To the extent possible under law, Pascal S. de Kloe has waived all copyright and related or neighboring rights to JWT. This work is published from The Netherlands. https://creativecommons.org/publicdomain/zero/1.0/legalcode ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] ; License : MIT License ``` MIT License Copyright (c) 2019 Yusuke Inuzuka Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` |
Added box/constbox/emoji_spin.gif.
cannot compute difference between binary files
Added box/constbox/error.mustache.
> > > > > > | 1 2 3 4 5 6 | <article> <header> <h1>{{ErrorTitle}}</h1> </header> {{ErrorText}} </article> |
Added box/constbox/form.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <article> <header> <h1>{{Heading}}</h1> </header> <form method="POST"> <div> <label for="title">Title</label> <input class="zs-input" type="text" id="title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus> </div> <div> <div> <label for="role">Role</label> <input class="zs-input" type="text" id="role" name="role" placeholder="role.." value="{{MetaRole}}"> </div> <label for="tags">Tags</label> <input class="zs-input" type="text" id="tags" name="tags" placeholder="#tag" value="{{MetaTags}}"> </div> <div> <label for="meta">Metadata</label> <textarea class="zs-input" id="meta" name="meta" rows="4" placeholder="metakey: metavalue"> {{#MetaPairsRest}} {{Key}}: {{Value}} {{/MetaPairsRest}} </textarea> </div> <div> <label for="syntax">Syntax</label> <input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}"> </div> <div> {{#IsTextContent}} <label for="content">Content</label> <textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea> {{/IsTextContent}} </div> <input class="zs-button" type="submit" value="Submit"> </form> </article> |
Added box/constbox/home.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | === Thank you for using Zettelstore! You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): * [[Zettelstore Version|00000000000001]] * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Runtime Configuration|00000000000100]] Additionally, you have to describe, what you have done before that error occurs and what you have expected instead. Please do not forget to include the error message, if there is one. Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"". Otherwise, only some zettel are linked. To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]: please set the metadata value of the key ''expert-mode'' to true. To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata. === Information about this zettel This zettel is your home zettel. It is part of the Zettelstore software itself. Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel. You can change the content of this zettel by clicking on ""Edit"" above. This allows you to customize your home zettel. Alternatively, you can designate another zettel as your home zettel. Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''. Its value is the identifier of the zettel that should act as the new home zettel. You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above. The identifier of this zettel is ''00010000000000''. If you provide a wrong identifier, this zettel will be shown as the home zettel. Take a look inside the manual for further details. |
Added box/constbox/info.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <article> <header> <h1>Information for Zettel {{Zid}}</h1> <a href="{{{WebURL}}}">Web</a> · <a href="{{{ContextURL}}}">Context</a> {{#CanWrite}} · <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}} {{#CanFolge}} · <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#CanCopy}} · <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanRename}}· <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}} {{#CanDelete}}· <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}} </header> <h2>Interpreted Metadata</h2> <table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table> {{#HasLinks}} <h2>References</h2> {{#HasLocLinks}} <h3>Local</h3> <ul> {{#LocLinks}} {{#Valid}}<li><a href="{{{Zid}}}">{{Zid}}</a></li>{{/Valid}} {{^Valid}}<li>{{Zid}}</li>{{/Valid}} {{/LocLinks}} </ul> {{/HasLocLinks}} {{#HasExtLinks}} <h3>External</h3> <ul> {{#ExtLinks}} <li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li> {{/ExtLinks}} </ul> {{/HasExtLinks}} {{/HasLinks}} <h2>Parts and format</h3> <table> {{#Matrix}} <tr> {{#Elements}}{{#HasURL}}<td><a href="{{{URL}}}">{{Text}}</td>{{/HasURL}}{{^HasURL}}<th>{{Text}}</th>{{/HasURL}} {{/Elements}} </tr> {{/Matrix}} </table> {{#HasShadowLinks}} <h2>Shadowed Boxes</h2> <ul>{{#ShadowLinks}}<li>{{.}}</li>{{/ShadowLinks}}</ul> {{/HasShadowLinks}} {{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} </article> |
Added box/constbox/license.txt.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | Copyright (c) 2020-2021 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the EU. The English version is included here. Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official translations of the other languages. ------------------------------------------------------------------------------- EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1. Definitions In this Licence, the following terms have the following meaning: — ‘The Licence’: this Licence. — ‘The Original Work’: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. — ‘Derivative Works’: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. — ‘The Work’: the Original Work or its Derivative Works. — ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify. — ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. — ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence. — ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. — ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence. — ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2. Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: — use the Work in any circumstance and for all usage, — reproduce the Work, — modify the Work, and make Derivative Works based upon the Work, — communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, — distribute the Work or copies thereof, — lend and rent the Work or copies thereof, — sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3. Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4. Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5. Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6. Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7. Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8. Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9. Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10. Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11. Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12. Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13. Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14. Jurisdiction Without prejudice to specific agreement between parties, — any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, — any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15. Applicable Law Without prejudice to specific agreement between parties, — this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, — this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: — GNU General Public License (GPL) v. 2, v. 3 — GNU Affero General Public License (AGPL) v. 3 — Open Software License (OSL) v. 2.1, v. 3.0 — Eclipse Public License (EPL) v. 1.0 — CeCILL v. 2.0, v. 2.1 — Mozilla Public Licence (MPL) v. 2 — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software — European Union Public Licence (EUPL) v. 1.1, v. 1.2 — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+) The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. |
Added box/constbox/listroles.mustache.
> > > > > > > > | 1 2 3 4 5 6 7 8 | <nav> <header> <h1>Currently used roles</h1> </header> <ul> {{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li> {{/Roles}}</ul> </nav> |
Added box/constbox/listtags.mustache.
> > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 | <nav> <header> <h1>Currently used tags</h1> <div class="zs-meta"> <a href="{{{ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}} </div> </header> {{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup> {{/Tags}} </nav> |
Added box/constbox/listzettel.mustache.
> > > > > > | 1 2 3 4 5 6 | <header> <h1>{{Title}}</h1> </header> <ul> {{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li> {{/Metas}}</ul> |
Added box/constbox/login.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <article> <header> <h1>{{Title}}</h1> </header> {{#Retry}} <div class="zs-indication zs-error">Wrong user name / password. Try again.</div> {{/Retry}} <form method="POST" action="?_format=html"> <div> <label for="username">User name</label> <input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus> </div> <div> <label for="password">Password</label> <input class="zs-input" type="password" id="password" name="password" placeholder="Your password.."> </div> <input class="zs-button" type="submit" value="Login"> </form> </article> |
Added box/constbox/newtoc.zettel.
> > > > | 1 2 3 4 | This zettel lists all zettel that should act as a template for new zettel. These zettel will be included in the ""New"" menu of the WebUI. * [[New Zettel|00000000090001]] * [[New User|00000000090002]] |
Added box/constbox/rename.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <article> <header> <h1>Rename Zettel {{.Zid}}</h1> </header> <p>Do you really want to rename this zettel?</p> <form method="POST"> <div> <label for="newid">New zettel id</label> <input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus> </div> <input type="hidden" id="curzid" name="curzid" value="{{Zid}}"> <input class="zs-button" type="submit" value="Rename"> </form> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> </article> |
Added box/constbox/zettel.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <article> <header> <h1>{{{HTMLTitle}}}</h1> <div class="zs-meta"> {{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> ·{{/CanWrite}} {{Zid}} · <a href="{{{InfoURL}}}">Info</a> · (<a href="{{{RoleURL}}}">{{RoleText}}</a>) {{#HasTags}}· {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}} {{#CanCopy}}· <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanFolge}}· <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#FolgeRefs}}<br>Folge: {{{FolgeRefs}}}{{/FolgeRefs}} {{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} {{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}} </div> </header> {{{Content}}} {{#HasBackLinks}} <details> <summary>Additional links to this zettel</summary> <ul> {{#BackLinks}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/BackLinks}} </ul> </details> {{/HasBackLinks}} </article> |
Added box/dirbox/dirbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox import ( "context" "errors" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getDirPath(u) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, err } dirSrvSpec, defWorker, maxWorker := getDirSrvInfo(u.Query().Get("type")) dp := dirBox{ number: cdata.Number, location: u.String(), readonly: getQueryBool(u, "readonly"), cdata: *cdata, dir: path, dirRescan: time.Duration(getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second, dirSrvSpec: dirSrvSpec, fSrvs: uint32(getQueryInt(u, "worker", 1, defWorker, maxWorker)), } return &dp, nil }) } type directoryServiceSpec int const ( _ directoryServiceSpec = iota dirSrvAny dirSrvSimple dirSrvNotify ) func getDirPath(u *url.URL) string { if u.Opaque != "" { return filepath.Clean(u.Opaque) } return filepath.Clean(u.Path) } func getQueryBool(u *url.URL, key string) bool { _, ok := u.Query()[key] return ok } func getQueryInt(u *url.URL, key string, min, def, max int) int { sVal := u.Query().Get(key) if sVal == "" { return def } iVal, err := strconv.Atoi(sVal) if err != nil { return def } if iVal < min { return min } if iVal > max { return max } return iVal } // dirBox uses a directory to store zettel as files. type dirBox struct { number int location string readonly bool cdata manager.ConnectData dir string dirRescan time.Duration dirSrvSpec directoryServiceSpec dirSrv directory.Service mustNotify bool fSrvs uint32 fCmds []chan fileCmd mxCmds sync.RWMutex } func (dp *dirBox) Location() string { return dp.location } func (dp *dirBox) Start(ctx context.Context) error { dp.mxCmds.Lock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := uint32(0); i < dp.fSrvs; i++ { cc := make(chan fileCmd) go fileService(i, cc) dp.fCmds = append(dp.fCmds, cc) } dp.setupDirService() dp.mxCmds.Unlock() if dp.dirSrv == nil { panic("No directory service") } return dp.dirSrv.Start() } func (dp *dirBox) Stop(ctx context.Context) error { dirSrv := dp.dirSrv dp.dirSrv = nil err := dirSrv.Stop() for _, c := range dp.fCmds { close(c) } return err } func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { if dp.mustNotify { if chci := dp.cdata.Notify; chci != nil { chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function sum := 2166136261 ^ uint32(zid) sum *= 16777619 sum ^= uint32(zid >> 32) sum *= 16777619 dp.mxCmds.RLock() defer dp.mxCmds.RUnlock() return dp.fCmds[sum%dp.fSrvs] } func (dp *dirBox) CanCreateZettel(ctx context.Context) bool { return !dp.readonly } func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, box.ErrReadOnly } entry, err := dp.dirSrv.GetNew() if err != nil { return id.Invalid, err } meta := zettel.Meta meta.Zid = entry.Zid dp.updateEntryFromMeta(entry, meta) err = setZettel(dp, entry, zettel) if err == nil { dp.dirSrv.UpdateEntry(entry) } dp.notifyChanged(box.OnUpdate, meta.Zid) return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return domain.Zettel{}, box.ErrNotFound } m, c, err := getMetaContent(dp, entry, zid) if err != nil { return domain.Zettel{}, err } dp.cleanupMeta(ctx, m) zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} return zettel, nil } func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return nil, box.ErrNotFound } m, err := getMeta(dp, entry, zid) if err != nil { return nil, err } dp.cleanupMeta(ctx, m) return m, nil } func (dp *dirBox) FetchZids(ctx context.Context) (id.Set, error) { entries, err := dp.dirSrv.GetEntries() if err != nil { return nil, err } result := id.NewSetCap(len(entries)) for _, entry := range entries { result[entry.Zid] = true } return result, nil } func (dp *dirBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { entries, err := dp.dirSrv.GetEntries() if err != nil { return nil, err } res = make([]*meta.Meta, 0, len(entries)) // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err1 := getMeta(dp, entry, entry.Zid) err = err1 if err != nil { continue } dp.cleanupMeta(ctx, m) dp.cdata.Enricher.Enrich(ctx, m, dp.number) if match(m) { res = append(res, m) } } if err != nil { return nil, err } return res, nil } func (dp *dirBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return !dp.readonly } func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { if dp.readonly { return box.ErrReadOnly } meta := zettel.Meta if !meta.Zid.IsValid() { return &box.ErrInvalidID{Zid: meta.Zid} } entry, err := dp.dirSrv.GetEntry(meta.Zid) if err != nil { return err } if !entry.IsValid() { // Existing zettel, but new in this box. entry = &directory.Entry{Zid: meta.Zid} dp.updateEntryFromMeta(entry, meta) } else if entry.MetaSpec == directory.MetaSpecNone { defaultMeta := filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) if !meta.Equal(defaultMeta, true) { dp.updateEntryFromMeta(entry, meta) dp.dirSrv.UpdateEntry(entry) } } err = setZettel(dp, entry, zettel) if err == nil { dp.notifyChanged(box.OnUpdate, meta.Zid) } return err } func (dp *dirBox) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) { entry.MetaSpec, entry.ContentExt = dp.calcSpecExt(meta) basePath := dp.calcBasePath(entry) if entry.MetaSpec == directory.MetaSpecFile { entry.MetaPath = basePath + ".meta" } entry.ContentPath = basePath + "." + entry.ContentExt entry.Duplicates = false } func (dp *dirBox) calcBasePath(entry *directory.Entry) string { p := entry.ContentPath if p == "" { return filepath.Join(dp.dir, entry.Zid.String()) } // ContentPath w/o the file extension return p[0 : len(p)-len(filepath.Ext(p))] } func (dp *dirBox) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { if m.YamlSep { return directory.MetaSpecHeader, "zettel" } syntax := m.GetDefault(meta.KeySyntax, "bin") switch syntax { case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: return directory.MetaSpecHeader, "zettel" } for _, s := range dp.cdata.Config.GetZettelFileSyntax() { if s == syntax { return directory.MetaSpecHeader, "zettel" } } return directory.MetaSpecFile, syntax } func (dp *dirBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return !dp.readonly } func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if curZid == newZid { return nil } curEntry, err := dp.dirSrv.GetEntry(curZid) if err != nil || !curEntry.IsValid() { return box.ErrNotFound } if dp.readonly { return box.ErrReadOnly } // Check whether zettel with new ID already exists in this box. if _, err = dp.GetMeta(ctx, newZid); err == nil { return &box.ErrInvalidID{Zid: newZid} } oldMeta, oldContent, err := getMetaContent(dp, curEntry, curZid) if err != nil { return err } newEntry := directory.Entry{ Zid: newZid, MetaSpec: curEntry.MetaSpec, MetaPath: renamePath(curEntry.MetaPath, curZid, newZid), ContentPath: renamePath(curEntry.ContentPath, curZid, newZid), ContentExt: curEntry.ContentExt, } if err = dp.dirSrv.RenameEntry(curEntry, &newEntry); err != nil { return err } oldMeta.Zid = newZid newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} if err = setZettel(dp, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameEntry(&newEntry, curEntry) return err } err = deleteZettel(dp, curEntry, curZid) if err == nil { dp.notifyChanged(box.OnDelete, curZid) dp.notifyChanged(box.OnUpdate, newZid) } return err } func (dp *dirBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { if dp.readonly { return false } entry, err := dp.dirSrv.GetEntry(zid) return err == nil && entry.IsValid() } func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return box.ErrReadOnly } entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return box.ErrNotFound } dp.dirSrv.DeleteEntry(zid) err = deleteZettel(dp, entry, zid) if err == nil { dp.notifyChanged(box.OnDelete, zid) } return err } func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly st.Zettel, _ = dp.dirSrv.NumEntries() } func (dp *dirBox) cleanupMeta(ctx context.Context, m *meta.Meta) { if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax()) } } func renamePath(path string, curID, newID id.Zid) string { dir, file := filepath.Split(path) if cur := curID.String(); strings.HasPrefix(file, cur) { file = newID.String() + file[len(cur):] return filepath.Join(dir, file) } return path } |
Added box/dirbox/directory/directory.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory interface of a dirstore. package directory import "zettelstore.de/z/domain/id" // Service is the interface of a directory service. type Service interface { Start() error Stop() error NumEntries() (int, error) GetEntries() ([]*Entry, error) GetEntry(zid id.Zid) (*Entry, error) GetNew() (*Entry, error) UpdateEntry(entry *Entry) error RenameEntry(curEntry, newEntry *Entry) error DeleteEntry(zid id.Zid) error } // MetaSpec defines all possibilities where meta data can be stored. type MetaSpec int // Constants for MetaSpec const ( _ MetaSpec = iota MetaSpecNone // no meta information MetaSpecFile // meta information is in meta file MetaSpecHeader // meta information is in header ) // Entry stores everything for a directory entry. type Entry struct { Zid id.Zid MetaSpec MetaSpec // location of meta information MetaPath string // file path of meta information ContentPath string // file path of zettel content ContentExt string // (normalized) file extension of zettel content Duplicates bool // multiple content files } // IsValid checks whether the entry is valid. func (e *Entry) IsValid() bool { return e != nil && e.Zid.IsValid() } |
Added box/dirbox/makedir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox import ( "zettelstore.de/z/box/dirbox/notifydir" "zettelstore.de/z/box/dirbox/simpledir" "zettelstore.de/z/kernel" ) func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) { for count := 0; count < 2; count++ { switch dirType { case kernel.BoxDirTypeNotify: return dirSrvNotify, 7, 1499 case kernel.BoxDirTypeSimple: return dirSrvSimple, 1, 1 default: dirType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) } } panic("unable to set default dir box type: " + dirType) } func (dp *dirBox) setupDirService() { switch dp.dirSrvSpec { case dirSrvSimple: dp.dirSrv = simpledir.NewService(dp.dir) dp.mustNotify = true default: dp.dirSrv = notifydir.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify) dp.mustNotify = false } } |
Added box/dirbox/notifydir/notifydir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/domain/id" ) // notifyService specifies a directory scan service. type notifyService struct { dirPath string rescanTime time.Duration done chan struct{} cmds chan dirCmd infos chan<- box.UpdateInfo } // NewService creates a new directory service. func NewService(directoryPath string, rescanTime time.Duration, chci chan<- box.UpdateInfo) directory.Service { srv := ¬ifyService{ dirPath: directoryPath, rescanTime: rescanTime, cmds: make(chan dirCmd), infos: chci, } return srv } // Start makes the directory service operational. func (srv *notifyService) Start() error { tick := make(chan struct{}) rawEvents := make(chan *fileEvent) events := make(chan *fileEvent) ready := make(chan int) go srv.directoryService(events, ready) go collectEvents(events, rawEvents) go watchDirectory(srv.dirPath, rawEvents, tick) if srv.done != nil { panic("src.done already set") } srv.done = make(chan struct{}) go ping(tick, srv.rescanTime, srv.done) <-ready return nil } // Stop stops the directory service. func (srv *notifyService) Stop() error { close(srv.done) srv.done = nil return nil } func (srv *notifyService) notifyChange(reason box.UpdateReason, zid id.Zid) { if chci := srv.infos; chci != nil { chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } // NumEntries returns the number of managed zettel. func (srv *notifyService) NumEntries() (int, error) { resChan := make(chan resNumEntries) srv.cmds <- &cmdNumEntries{resChan} return <-resChan, nil } // GetEntries returns an unsorted list of all current directory entries. func (srv *notifyService) GetEntries() ([]*directory.Entry, error) { resChan := make(chan resGetEntries) srv.cmds <- &cmdGetEntries{resChan} return <-resChan, nil } // GetEntry returns the entry with the specified zettel id. If there is no such // zettel id, an empty entry is returned. func (srv *notifyService) GetEntry(zid id.Zid) (*directory.Entry, error) { resChan := make(chan resGetEntry) srv.cmds <- &cmdGetEntry{zid, resChan} return <-resChan, nil } // GetNew returns an entry with a new zettel id. func (srv *notifyService) GetNew() (*directory.Entry, error) { resChan := make(chan resNewEntry) srv.cmds <- &cmdNewEntry{resChan} result := <-resChan return result.entry, result.err } // UpdateEntry notifies the directory of an updated entry. func (srv *notifyService) UpdateEntry(entry *directory.Entry) error { resChan := make(chan struct{}) srv.cmds <- &cmdUpdateEntry{entry, resChan} <-resChan return nil } // RenameEntry notifies the directory of an renamed entry. func (srv *notifyService) RenameEntry(curEntry, newEntry *directory.Entry) error { resChan := make(chan resRenameEntry) srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan} return <-resChan } // DeleteEntry removes a zettel id from the directory of entries. func (srv *notifyService) DeleteEntry(zid id.Zid) error { resChan := make(chan struct{}) srv.cmds <- &cmdDeleteEntry{zid, resChan} <-resChan return nil } |
Added box/dirbox/notifydir/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "log" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/domain/id" ) // ping sends every tick a signal to reload the directory list func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { ticker := time.NewTicker(rescanTime) defer close(tick) for { select { case _, ok := <-ticker.C: if !ok { return } tick <- struct{}{} case _, ok := <-done: if !ok { ticker.Stop() return } } } } func newEntry(ev *fileEvent) *directory.Entry { de := new(directory.Entry) de.Zid = ev.zid updateEntry(de, ev) return de } func updateEntry(de *directory.Entry, ev *fileEvent) { if ev.ext == "meta" { de.MetaSpec = directory.MetaSpecFile de.MetaPath = ev.path return } if de.ContentExt != "" && de.ContentExt != ev.ext { de.Duplicates = true return } if de.MetaSpec != directory.MetaSpecFile { if ev.ext == "zettel" { de.MetaSpec = directory.MetaSpecHeader } else { de.MetaSpec = directory.MetaSpecNone } } de.ContentPath = ev.path de.ContentExt = ev.ext } type dirMap map[id.Zid]*directory.Entry func dirMapUpdate(dm dirMap, ev *fileEvent) { de := dm[ev.zid] if de == nil { dm[ev.zid] = newEntry(ev) return } updateEntry(de, ev) } func deleteFromMap(dm dirMap, ev *fileEvent) { if ev.ext == "meta" { if entry, ok := dm[ev.zid]; ok { if entry.MetaSpec == directory.MetaSpecFile { entry.MetaSpec = directory.MetaSpecNone return } } } delete(dm, ev.zid) } // directoryService is the main service. func (srv *notifyService) directoryService(events <-chan *fileEvent, ready chan<- int) { curMap := make(dirMap) var newMap dirMap for { select { case ev, ok := <-events: if !ok { return } switch ev.status { case fileStatusReloadStart: newMap = make(dirMap) case fileStatusReloadEnd: curMap = newMap newMap = nil if ready != nil { ready <- len(curMap) close(ready) ready = nil } srv.notifyChange(box.OnReload, id.Invalid) case fileStatusError: log.Println("DIRBOX", "ERROR", ev.err) case fileStatusUpdate: srv.processFileUpdateEvent(ev, curMap, newMap) case fileStatusDelete: srv.processFileDeleteEvent(ev, curMap, newMap) } case cmd, ok := <-srv.cmds: if ok { cmd.run(curMap) } } } } func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { dirMapUpdate(newMap, ev) } else { dirMapUpdate(curMap, ev) srv.notifyChange(box.OnUpdate, ev.zid) } } func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { deleteFromMap(newMap, ev) } else { deleteFromMap(curMap, ev) srv.notifyChange(box.OnDelete, ev.zid) } } type dirCmd interface { run(m dirMap) } type cmdNumEntries struct { result chan<- resNumEntries } type resNumEntries = int func (cmd *cmdNumEntries) run(m dirMap) { cmd.result <- len(m) } type cmdGetEntries struct { result chan<- resGetEntries } type resGetEntries []*directory.Entry func (cmd *cmdGetEntries) run(m dirMap) { res := make([]*directory.Entry, len(m)) i := 0 for _, de := range m { entry := *de res[i] = &entry i++ } cmd.result <- res } type cmdGetEntry struct { zid id.Zid result chan<- resGetEntry } type resGetEntry = *directory.Entry func (cmd *cmdGetEntry) run(m dirMap) { entry := m[cmd.zid] if entry == nil { cmd.result <- nil } else { result := *entry cmd.result <- &result } } type cmdNewEntry struct { result chan<- resNewEntry } type resNewEntry struct { entry *directory.Entry err error } func (cmd *cmdNewEntry) run(m dirMap) { zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := m[zid] return !ok, nil }) if err != nil { cmd.result <- resNewEntry{nil, err} return } entry := &directory.Entry{Zid: zid} m[zid] = entry cmd.result <- resNewEntry{&directory.Entry{Zid: zid}, nil} } type cmdUpdateEntry struct { entry *directory.Entry result chan<- struct{} } func (cmd *cmdUpdateEntry) run(m dirMap) { entry := *cmd.entry m[entry.Zid] = &entry cmd.result <- struct{}{} } type cmdRenameEntry struct { curEntry *directory.Entry newEntry *directory.Entry result chan<- resRenameEntry } type resRenameEntry = error func (cmd *cmdRenameEntry) run(m dirMap) { newEntry := *cmd.newEntry newZid := newEntry.Zid if _, found := m[newZid]; found { cmd.result <- &box.ErrInvalidID{Zid: newZid} return } delete(m, cmd.curEntry.Zid) m[newZid] = &newEntry cmd.result <- nil } type cmdDeleteEntry struct { zid id.Zid result chan<- struct{} } func (cmd *cmdDeleteEntry) run(m dirMap) { delete(m, cmd.zid) cmd.result <- struct{}{} } |
Added box/dirbox/notifydir/watch.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "os" "path/filepath" "regexp" "time" "github.com/fsnotify/fsnotify" "zettelstore.de/z/domain/id" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type fileStatus int const ( fileStatusNone fileStatus = iota fileStatusReloadStart fileStatusReloadEnd fileStatusError fileStatusUpdate fileStatusDelete ) type fileEvent struct { status fileStatus path string // Full file path zid id.Zid ext string // File extension err error // Error if Status == fileStatusError } type sendResult int const ( sendDone sendResult = iota sendReload sendExit ) func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) { defer close(events) var watcher *fsnotify.Watcher defer func() { if watcher != nil { watcher.Close() } }() sendEvent := func(ev *fileEvent) sendResult { select { case events <- ev: case _, ok := <-tick: if ok { return sendReload } return sendExit } return sendDone } sendError := func(err error) sendResult { return sendEvent(&fileEvent{status: fileStatusError, err: err}) } sendFileEvent := func(status fileStatus, path string, match []string) sendResult { zid, err := id.Parse(match[1]) if err != nil { return sendDone } event := &fileEvent{ status: status, path: path, zid: zid, ext: match[3], } return sendEvent(event) } reloadStartEvent := &fileEvent{status: fileStatusReloadStart} reloadEndEvent := &fileEvent{status: fileStatusReloadEnd} reloadFiles := func() bool { entries, err := os.ReadDir(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } return true } if res := sendEvent(reloadStartEvent); res != sendDone { return res == sendReload } if watcher != nil { watcher.Close() } watcher, err = fsnotify.NewWatcher() if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } for _, entry := range entries { if entry.IsDir() { continue } if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := entry.Name() match := matchValidFileName(name) if len(match) > 0 { path := filepath.Join(directory, name) if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } } if watcher != nil { err = watcher.Add(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } } if res := sendEvent(reloadEndEvent); res != sendDone { return res == sendReload } return true } handleEvents := func() bool { const createOps = fsnotify.Create | fsnotify.Write const deleteOps = fsnotify.Remove | fsnotify.Rename for { select { case wevent, ok := <-watcher.Events: if !ok { return false } path := filepath.Clean(wevent.Name) match := matchValidFileName(filepath.Base(path)) if len(match) == 0 { continue } if wevent.Op&createOps != 0 { if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() { continue } if res := sendFileEvent( fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } if wevent.Op&deleteOps != 0 { if res := sendFileEvent( fileStatusDelete, path, match); res != sendDone { return res == sendReload } } case err, ok := <-watcher.Errors: if !ok { return false } if res := sendError(err); res != sendDone { return res == sendReload } case _, ok := <-tick: return ok } } } for { if !reloadFiles() { return } if watcher == nil { if _, ok := <-tick; !ok { return } } else { if !handleEvents() { return } } } } func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) { for _, ev := range events { if ev.status != fileStatusNone { out <- ev } } } func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent { switch ev.status { case fileStatusNone: return events case fileStatusReloadStart: events = events[0:0] case fileStatusUpdate, fileStatusDelete: if len(events) > 0 && mergeEvents(events, ev) { return events } } return append(events, ev) } func mergeEvents(events []*fileEvent, ev *fileEvent) bool { for i := len(events) - 1; i >= 0; i-- { oev := events[i] switch oev.status { case fileStatusReloadStart, fileStatusReloadEnd: return false case fileStatusUpdate, fileStatusDelete: if ev.path == oev.path { if ev.status == oev.status { return true } oev.status = fileStatusNone return false } } } return false } func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) { defer close(out) var sendTime time.Time sendTimeSet := false ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() events := make([]*fileEvent, 0, 32) buffer := false for { select { case ev, ok := <-in: if !ok { sendCollectedEvents(out, events) return } if ev.status == fileStatusReloadStart { buffer = false events = events[0:0] } if buffer { if !sendTimeSet { sendTime = time.Now().Add(1500 * time.Millisecond) sendTimeSet = true } events = addEvent(events, ev) if len(events) > 1024 { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } continue } out <- ev if ev.status == fileStatusReloadEnd { buffer = true } case now := <-ticker.C: if sendTimeSet && now.After(sendTime) { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } } } } |
Added box/dirbox/notifydir/watch_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import "testing" func sameStringSlices(sl1, sl2 []string) bool { if len(sl1) != len(sl2) { return false } for i := 0; i < len(sl1); i++ { if sl1[i] != sl2[i] { return false } } return true } func TestMatchValidFileName(t *testing.T) { t.Parallel() testcases := []struct { name string exp []string }{ {"", []string{}}, {".txt", []string{}}, {"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}}, } for i, tc := range testcases { got := matchValidFileName(tc.name) if len(got) == 0 { if len(tc.exp) > 0 { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } else { if got[0] != tc.name { t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got) } if !sameStringSlices(got[1:], tc.exp) { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } } } |
Added box/dirbox/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox import ( "os" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/box/filebox" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) func fileService(num uint32, cmds <-chan fileCmd) { for cmd := range cmds { cmd.run() } } type fileCmd interface { run() } // COMMAND: getMeta ---------------------------------------- // // Retrieves the meta data from a zettel. func getMeta(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) { rc := make(chan resGetMeta) dp.getFileChan(zid) <- &fileGetMeta{entry, rc} res := <-rc close(rc) return res.meta, res.err } type fileGetMeta struct { entry *directory.Entry rc chan<- resGetMeta } type resGetMeta struct { meta *meta.Meta err error } func (cmd *fileGetMeta) run() { entry := cmd.entry var m *meta.Meta var err error switch entry.MetaSpec { case directory.MetaSpecFile: m, err = parseMetaFile(entry.Zid, entry.MetaPath) case directory.MetaSpecHeader: m, _, err = parseMetaContentFile(entry.Zid, entry.ContentPath) default: m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMeta{m, err} } // COMMAND: getMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. func getMetaContent(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, string, error) { rc := make(chan resGetMetaContent) dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc} res := <-rc close(rc) return res.meta, res.content, res.err } type fileGetMetaContent struct { entry *directory.Entry rc chan<- resGetMetaContent } type resGetMetaContent struct { meta *meta.Meta content string err error } func (cmd *fileGetMetaContent) run() { var m *meta.Meta var content string var err error entry := cmd.entry switch entry.MetaSpec { case directory.MetaSpecFile: m, err = parseMetaFile(entry.Zid, entry.MetaPath) if err == nil { content, err = readFileContent(entry.ContentPath) } case directory.MetaSpecHeader: m, content, err = parseMetaContentFile(entry.Zid, entry.ContentPath) default: m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) content, err = readFileContent(entry.ContentPath) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMetaContent{m, content, err} } // COMMAND: setZettel ---------------------------------------- // // Writes a new or exsting zettel. func setZettel(dp *dirBox, entry *directory.Entry, zettel domain.Zettel) error { rc := make(chan resSetZettel) dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} err := <-rc close(rc) return err } type fileSetZettel struct { entry *directory.Entry zettel domain.Zettel rc chan<- resSetZettel } type resSetZettel = error func (cmd *fileSetZettel) run() { var err error switch cmd.entry.MetaSpec { case directory.MetaSpecFile: err = cmd.runMetaSpecFile() case directory.MetaSpecHeader: err = cmd.runMetaSpecHeader() case directory.MetaSpecNone: // TODO: if meta has some additional infos: write meta to new .meta; // update entry in dir err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) default: panic("TODO: ???") } cmd.rc <- err } func (cmd *fileSetZettel) runMetaSpecFile() error { f, err := openFileWrite(cmd.entry.MetaPath) if err == nil { err = writeFileZid(f, cmd.zettel.Meta.Zid) if err == nil { _, err = cmd.zettel.Meta.Write(f, true) if err1 := f.Close(); err == nil { err = err1 } if err == nil { err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) } } } return err } func (cmd *fileSetZettel) runMetaSpecHeader() error { f, err := openFileWrite(cmd.entry.ContentPath) if err == nil { err = writeFileZid(f, cmd.zettel.Meta.Zid) if err == nil { _, err = cmd.zettel.Meta.WriteAsHeader(f, true) if err == nil { _, err = f.WriteString(cmd.zettel.Content.AsString()) if err1 := f.Close(); err == nil { err = err1 } } } } return err } // COMMAND: deleteZettel ---------------------------------------- // // Deletes an existing zettel. func deleteZettel(dp *dirBox, entry *directory.Entry, zid id.Zid) error { rc := make(chan resDeleteZettel) dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc} err := <-rc close(rc) return err } type fileDeleteZettel struct { entry *directory.Entry rc chan<- resDeleteZettel } type resDeleteZettel = error func (cmd *fileDeleteZettel) run() { var err error switch cmd.entry.MetaSpec { case directory.MetaSpecFile: err1 := os.Remove(cmd.entry.MetaPath) err = os.Remove(cmd.entry.ContentPath) if err == nil { err = err1 } case directory.MetaSpecHeader: err = os.Remove(cmd.entry.ContentPath) case directory.MetaSpecNone: err = os.Remove(cmd.entry.ContentPath) default: panic("TODO: ???") } cmd.rc <- err } // Utility functions ---------------------------------------- func readFileContent(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } return string(data), nil } func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) { src, err := readFileContent(path) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) { src, err := readFileContent(path) if err != nil { return nil, "", err } inp := input.NewInput(src) meta := meta.NewFromInput(zid, inp) return meta, src[inp.Pos:], nil } func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) { filebox.CleanupMeta( m, entry.Zid, entry.ContentExt, entry.MetaSpec == directory.MetaSpecFile, entry.Duplicates, ) } func openFileWrite(path string) (*os.File, error) { return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) } func writeFileZid(f *os.File, zid id.Zid) error { _, err := f.WriteString("id: ") if err == nil { _, err = f.Write(zid.Bytes()) if err == nil { _, err = f.WriteString("\n") } } return err } func writeFileContent(path, content string) error { f, err := openFileWrite(path) if err == nil { _, err = f.WriteString(content) if err1 := f.Close(); err == nil { err = err1 } } return err } |
Added box/dirbox/simpledir/simpledir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package simpledir manages the directory part of a dirstore. package simpledir import ( "os" "path/filepath" "regexp" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/domain/id" ) // simpleService specifies a directory service without scanning. type simpleService struct { dirPath string mx sync.Mutex } // NewService creates a new directory service. func NewService(directoryPath string) directory.Service { return &simpleService{ dirPath: directoryPath, } } func (ss *simpleService) Start() error { ss.mx.Lock() defer ss.mx.Unlock() _, err := os.ReadDir(ss.dirPath) return err } func (ss *simpleService) Stop() error { return nil } func (ss *simpleService) NumEntries() (int, error) { ss.mx.Lock() defer ss.mx.Unlock() entries, err := ss.getEntries() if err == nil { return len(entries), nil } return 0, err } func (ss *simpleService) GetEntries() ([]*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() entrySet, err := ss.getEntries() if err != nil { return nil, err } result := make([]*directory.Entry, 0, len(entrySet)) for _, entry := range entrySet { result = append(result, entry) } return result, nil } func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) { dirEntries, err := os.ReadDir(ss.dirPath) if err != nil { return nil, err } entrySet := make(map[id.Zid]*directory.Entry) for _, dirEntry := range dirEntries { if dirEntry.IsDir() { continue } if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := dirEntry.Name() match := matchValidFileName(name) if len(match) == 0 { continue } zid, err := id.Parse(match[1]) if err != nil { continue } var entry *directory.Entry if e, ok := entrySet[zid]; ok { entry = e } else { entry = &directory.Entry{Zid: zid} entrySet[zid] = entry } updateEntry(entry, filepath.Join(ss.dirPath, name), match[3]) } return entrySet, nil } var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } func updateEntry(entry *directory.Entry, path, ext string) { if ext == "meta" { entry.MetaSpec = directory.MetaSpecFile entry.MetaPath = path } else if entry.ContentExt != "" && entry.ContentExt != ext { entry.Duplicates = true } else { if entry.MetaSpec != directory.MetaSpecFile { if ext == "zettel" { entry.MetaSpec = directory.MetaSpecHeader } else { entry.MetaSpec = directory.MetaSpecNone } } entry.ContentPath = path entry.ContentExt = ext } } func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() return ss.getEntry(zid) } func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) { pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*" paths, err := filepath.Glob(pattern) if err != nil { return nil, err } if len(paths) == 0 { return nil, nil } entry := &directory.Entry{Zid: zid} for _, path := range paths { ext := filepath.Ext(path) if len(ext) > 0 && ext[0] == '.' { ext = ext[1:] } updateEntry(entry, path, ext) } return entry, nil } func (ss *simpleService) GetNew() (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { entry, err := ss.getEntry(zid) if err != nil { return false, nil } return !entry.IsValid(), nil }) if err != nil { return nil, err } return &directory.Entry{Zid: zid}, nil } func (ss *simpleService) UpdateEntry(entry *directory.Entry) error { // Nothing to to, since the actual file update is done by dirbox. return nil } func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error { // Nothing to to, since the actual file rename is done by dirbox. return nil } func (ss *simpleService) DeleteEntry(zid id.Zid) error { // Nothing to to, since the actual file delete is done by dirbox. return nil } |
Added box/filebox/filebox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox import ( "errors" "net/url" "path/filepath" "strings" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String()) } return &zipBox{ number: cdata.Number, name: path, enricher: cdata.Enricher, }, nil }) } func getFilepathFromURL(u *url.URL) string { name := u.Opaque if name == "" { name = u.Path } components := strings.Split(name, "/") fileName := filepath.Join(components...) if len(components) > 0 && components[0] == "" { return "/" + fileName } return fileName } var alternativeSyntax = map[string]string{ "htm": "html", } func calculateSyntax(ext string) string { ext = strings.ToLower(ext) if syntax, ok := alternativeSyntax[ext]; ok { return syntax } return ext } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, zid.String()) m.Set(meta.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta, duplicates bool) { if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { m.Set(meta.KeyTitle, zid.String()) } if inMeta { if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } m.Set(meta.KeySyntax, syntax) } } if duplicates { m.Set(meta.KeyDuplicates, meta.ValueTrue) } } |
Added box/filebox/zipbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox import ( "archive/zip" "context" "io" "regexp" "strings" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/search" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type zipEntry struct { metaName string contentName string contentExt string // (normalized) file extension of zettel content metaInHeader bool } type zipBox struct { number int name string enricher box.Enricher zettel map[id.Zid]*zipEntry // no lock needed, because read-only after creation } func (zp *zipBox) Location() string { if strings.HasPrefix(zp.name, "/") { return "file://" + zp.name } return "file:" + zp.name } func (zp *zipBox) Start(ctx context.Context) error { reader, err := zip.OpenReader(zp.name) if err != nil { return err } defer reader.Close() zp.zettel = make(map[id.Zid]*zipEntry) for _, f := range reader.File { match := matchValidFileName(f.Name) if len(match) < 1 { continue } zid, err := id.Parse(match[1]) if err != nil { continue } zp.addFile(zid, f.Name, match[3]) } return nil } func (zp *zipBox) addFile(zid id.Zid, name, ext string) { entry := zp.zettel[zid] if entry == nil { entry = &zipEntry{} zp.zettel[zid] = entry } switch ext { case "zettel": if entry.contentExt == "" { entry.contentName = name entry.contentExt = ext entry.metaInHeader = true } case "meta": entry.metaName = name entry.metaInHeader = false default: if entry.contentExt == "" { entry.contentExt = ext entry.contentName = name } } } func (zp *zipBox) Stop(ctx context.Context) error { zp.zettel = nil return nil } func (zp *zipBox) CanCreateZettel(ctx context.Context) bool { return false } func (zp *zipBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { return id.Invalid, box.ErrReadOnly } func (zp *zipBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry, ok := zp.zettel[zid] if !ok { return domain.Zettel{}, box.ErrNotFound } reader, err := zip.OpenReader(zp.name) if err != nil { return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta var src string var inMeta bool if entry.metaInHeader { src, err = readZipFileContent(reader, entry.contentName) if err != nil { return domain.Zettel{}, err } inp := input.NewInput(src) m = meta.NewFromInput(zid, inp) src = src[inp.Pos:] } else if metaName := entry.metaName; metaName != "" { m, err = readZipMetaFile(reader, zid, metaName) if err != nil { return domain.Zettel{}, err } src, err = readZipFileContent(reader, entry.contentName) if err != nil { return domain.Zettel{}, err } inMeta = true } else { m = CalcDefaultMeta(zid, entry.contentExt) } CleanupMeta(m, zid, entry.contentExt, inMeta, false) return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil } func (zp *zipBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { entry, ok := zp.zettel[zid] if !ok { return nil, box.ErrNotFound } reader, err := zip.OpenReader(zp.name) if err != nil { return nil, err } defer reader.Close() return readZipMeta(reader, zid, entry) } func (zp *zipBox) FetchZids(ctx context.Context) (id.Set, error) { result := id.NewSetCap(len(zp.zettel)) for zid := range zp.zettel { result[zid] = true } return result, nil } func (zp *zipBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { reader, err := zip.OpenReader(zp.name) if err != nil { return nil, err } defer reader.Close() for zid, entry := range zp.zettel { m, err := readZipMeta(reader, zid, entry) if err != nil { continue } zp.enricher.Enrich(ctx, m, zp.number) if match(m) { res = append(res, m) } } return res, nil } func (zp *zipBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return false } func (zp *zipBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return box.ErrReadOnly } func (zp *zipBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := zp.zettel[zid] return !ok } func (zp *zipBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := zp.zettel[curZid]; ok { return box.ErrReadOnly } return box.ErrNotFound } func (zp *zipBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } func (zp *zipBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := zp.zettel[zid]; ok { return box.ErrReadOnly } return box.ErrNotFound } func (zp *zipBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(zp.zettel) } func readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *zipEntry) (m *meta.Meta, err error) { var inMeta bool if entry.metaInHeader { m, err = readZipMetaFile(reader, zid, entry.contentName) } else if metaName := entry.metaName; metaName != "" { m, err = readZipMetaFile(reader, zid, entry.metaName) inMeta = true } else { m = CalcDefaultMeta(zid, entry.contentExt) } if err == nil { CleanupMeta(m, zid, entry.contentExt, inMeta, false) } return m, err } func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) { src, err := readZipFileContent(reader, name) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func readZipFileContent(reader *zip.ReadCloser, name string) (string, error) { f, err := reader.Open(name) if err != nil { return "", err } defer f.Close() buf, err := io.ReadAll(f) if err != nil { return "", err } return string(buf), nil } |
Added box/helper.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box import ( "time" "zettelstore.de/z/domain/id" ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) found, err := testZid(zid) if err != nil { return id.Invalid, err } if found { return zid, nil } // TODO: do not wait here unconditionally. time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrConflict } |
Added box/manager/anteroom.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "sync" "zettelstore.de/z/domain/id" ) type arAction int const ( arNothing arAction = iota arReload arUpdate arDelete ) type anteroom struct { num uint64 next *anteroom waiting map[id.Zid]arAction curLoad int reload bool } type anterooms struct { mx sync.Mutex nextNum uint64 first *anteroom last *anteroom maxLoad int } func newAnterooms(maxLoad int) *anterooms { return &anterooms{maxLoad: maxLoad} } func (ar *anterooms) Enqueue(zid id.Zid, action arAction) { if !zid.IsValid() || action == arNothing || action == arReload { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { ar.first = ar.makeAnteroom(zid, action) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } a, ok := room.waiting[zid] if !ok { continue } switch action { case a: return case arUpdate: room.waiting[zid] = action case arDelete: room.waiting[zid] = action } return } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { room.waiting[zid] = action room.curLoad++ return } room := ar.makeAnteroom(zid, action) ar.last.next = room ar.last = room } func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { c := ar.maxLoad if c == 0 { c = 100 } waiting := make(map[id.Zid]arAction, c) waiting[zid] = action ar.nextNum++ return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false} } func (ar *anterooms) Reset() { ar.mx.Lock() defer ar.mx.Unlock() ar.first = ar.makeAnteroom(id.Invalid, arReload) ar.last = ar.first } func (ar *anterooms) Reload(newZids id.Set) uint64 { ar.mx.Lock() defer ar.mx.Unlock() newWaiting := createWaitingSet(newZids, arUpdate) ar.deleteReloadedRooms() if ns := len(newWaiting); ns > 0 { ar.nextNum++ ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} if ar.first.next == nil { ar.last = ar.first } return ar.nextNum } ar.first = nil ar.last = nil return 0 } func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction { waitingSet := make(map[id.Zid]arAction, len(zids)) for zid := range zids { if zid.IsValid() { waitingSet[zid] = action } } return waitingSet } func (ar *anterooms) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room if room == nil { ar.last = nil } } func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) { ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { return arNothing, id.Invalid, 0 } for zid, action := range ar.first.waiting { roomNo := ar.first.num delete(ar.first.waiting, zid) if len(ar.first.waiting) == 0 { ar.first = ar.first.next if ar.first == nil { ar.last = nil } } return action, zid, roomNo } return arNothing, id.Invalid, 0 } |
Added box/manager/anteroom_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "testing" "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { t.Parallel() ar := newAnterooms(2) ar.Enqueue(id.Zid(1), arUpdate) action, zid, rno := ar.Dequeue() if zid != id.Zid(1) || action != arUpdate || rno != 1 { t.Errorf("Expected arUpdate/1/1, but got %v/%v/%v", action, zid, rno) } action, zid, _ = ar.Dequeue() if zid != id.Invalid && action != arDelete { t.Errorf("Expected invalid Zid, but got %v", zid) } ar.Enqueue(id.Zid(1), arUpdate) ar.Enqueue(id.Zid(2), arUpdate) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } ar.Enqueue(id.Zid(3), arUpdate) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { action, _, _ := ar.Dequeue() if action == arNothing { break } } if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } } func TestReset(t *testing.T) { t.Parallel() ar := newAnterooms(1) ar.Enqueue(id.Zid(1), arUpdate) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.NewSet(3, 4)) ar.Enqueue(id.Zid(5), arUpdate) ar.Enqueue(id.Zid(5), arDelete) ar.Enqueue(id.Zid(5), arDelete) ar.Enqueue(id.Zid(5), arUpdate) if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { t.Errorf("Expected 2 rooms") } action, zid1, _ := ar.Dequeue() if action != arUpdate { t.Errorf("Expected arUpdate, but got %v", action) } action, zid2, _ := ar.Dequeue() if action != arUpdate { t.Errorf("Expected arUpdate, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid, _ = ar.Dequeue() if zid != id.Zid(5) || action != arUpdate { t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(id.NewSet(id.Zid(6))) action, zid, _ = ar.Dequeue() if zid != id.Zid(6) || action != arUpdate { t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Enqueue(id.Zid(8), arUpdate) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } |
Added box/manager/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "errors" "sort" "strings" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // Conatains all box.Box related functions // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } var sb strings.Builder for i := 0; i < len(mgr.boxes)-2; i++ { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.boxes[i].Location()) } return sb.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return id.Invalid, box.ErrStopped } return mgr.boxes[0].CreateZettel(ctx, zettel) } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return domain.Zettel{}, box.ErrStopped } for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound { if err == nil { mgr.Enrich(ctx, z.Meta, i+1) } return z, err } } return domain.Zettel{}, box.ErrNotFound } // GetAllZettel retrieves a specific zettel from all managed boxes. func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } var result []domain.Zettel for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err == nil { mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } return result, nil } // GetMeta retrieves just the meta data of a specific zettel. func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } for i, p := range mgr.boxes { if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound { if err == nil { mgr.Enrich(ctx, m, i+1) } return m, err } } return nil, box.ErrNotFound } // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } var result []*meta.Meta for i, p := range mgr.boxes { if m, err := p.GetMeta(ctx, zid); err == nil { mgr.Enrich(ctx, m, i+1) result = append(result, m) } } return result, nil } // FetchZids returns the set of all zettel identifer managed by the box. func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } for _, p := range mgr.boxes { zids, err := p.FetchZids(ctx) if err != nil { return nil, err } if result == nil { result = zids } else if len(result) <= len(zids) { for zid := range result { zids[zid] = true } result = zids } else { for zid := range zids { result[zid] = true } } } return result, nil } // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } var result []*meta.Meta match := s.CompileMatch(mgr) for _, p := range mgr.boxes { selected, err := p.SelectMeta(ctx, match) if err != nil { return nil, err } sort.Slice(selected, func(i, j int) bool { return selected[i].Zid > selected[j].Zid }) if len(result) == 0 { result = selected } else { result = box.MergeSorted(result, selected) } } if s == nil { return result, nil } return s.Sort(result), nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel) } // UpdateZettel updates an existing zettel. func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return box.ErrStopped } // Remove all (computed) properties from metadata before storing the zettel. zettel.Meta = zettel.Meta.Clone() for _, p := range zettel.Meta.PairsRest(true) { if mgr.propertyKeys[p.Key] { zettel.Meta.Delete(p.Key) } } return mgr.boxes[0].UpdateZettel(ctx, zettel) } // AllowRenameZettel returns true, if box will not disallow renaming the zettel. func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return false } for _, p := range mgr.boxes { if !p.AllowRenameZettel(ctx, zid) { return false } } return true } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return box.ErrStopped } for i, p := range mgr.boxes { err := p.RenameZettel(ctx, curZid, newZid) if err != nil && !errors.Is(err, box.ErrNotFound) { for j := 0; j < i; j++ { mgr.boxes[j].RenameZettel(ctx, newZid, curZid) } return err } } return nil } // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return false } for _, p := range mgr.boxes { if p.CanDeleteZettel(ctx, zid) { return true } } return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return box.ErrStopped } for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { return nil } if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) { return err } } return box.ErrNotFound } |
Added box/manager/collect.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/strfun" ) type collectData struct { refs id.Set words store.WordSet urls store.WordSet } func (data *collectData) initialize() { data.refs = id.NewSet() data.words = store.NewWordSet() data.urls = store.NewWordSet() } func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) { ast.WalkBlockSlice(data, zn.Ast) } func collectInlineIndexData(ins ast.InlineSlice, data *collectData) { ast.WalkInlineSlice(data, ins) } func (data *collectData) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.VerbatimNode: for _, line := range n.Lines { data.addText(line) } case *ast.TextNode: data.addText(n.Text) case *ast.TagNode: data.addText(n.Tag) case *ast.LinkNode: data.addRef(n.Ref) case *ast.ImageNode: data.addRef(n.Ref) case *ast.LiteralNode: data.addText(n.Text) } return data } func (data *collectData) addText(s string) { for _, word := range strfun.NormalizeWords(s) { data.words.Add(word) } } func (data *collectData) addRef(ref *ast.Reference) { if ref == nil { return } if ref.IsExternal() { data.urls.Add(strings.ToLower(ref.Value)) } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { data.refs[zid] = true } } |
Added box/manager/enrich.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "strconv" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { if box.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update meta data return } m.Set(meta.KeyBoxNumber, strconv.Itoa(boxNumber)) computePublished(m) mgr.idxStore.Enrich(ctx, m) } func computePublished(m *meta.Meta) { if _, ok := m.Get(meta.KeyPublished); ok { return } if modified, ok := m.Get(meta.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { m.Set(meta.KeyPublished, modified) return } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { m.Set(meta.KeyPublished, zid) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } |
Added box/manager/indexer.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) id.Set { return mgr.idxStore.SearchEqual(word) } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchPrefix(prefix string) id.Set { return mgr.idxStore.SearchPrefix(prefix) } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchSuffix(suffix string) id.Set { return mgr.idxStore.SearchSuffix(suffix) } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchContains(s string) id.Set { return mgr.idxStore.SearchContains(s) } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("Indexer", r) go mgr.idxIndexer() } }() timerDuration := 15 * time.Second timer := time.NewTimer(timerDuration) ctx := box.NoEnrichContext(context.Background()) for { mgr.idxWorkService(ctx) if !mgr.idxSleepService(timer, timerDuration) { return } } } func (mgr *Manager) idxWorkService(ctx context.Context) { var roomNum uint64 var start time.Time for { switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: roomNum = 0 zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() if rno := mgr.idxAr.Reload(zids); rno > 0 { roomNum = rno } mgr.idxMx.Lock() mgr.idxLastReload = time.Now() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } case arUpdate: zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // TODO: on some errors put the zid into a "try later" set continue } mgr.idxMx.Lock() if arRoomNum == roomNum { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxUpdateZettel(ctx, zettel) case arDelete: if _, err := mgr.GetMeta(ctx, zid); err == nil { // Zettel was not deleted. This might occur, if zettel was // deleted in secondary dirbox, but is still present in // first dirbox (or vice versa). Re-index zettel in case // a hidden zettel was recovered mgr.idxAr.Enqueue(zid, arUpdate) } mgr.idxMx.Lock() mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxDeleteZettel(zid) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { select { case _, ok := <-mgr.idxReady: if !ok { return false } case _, ok := <-timer.C: if !ok { return false } timer.Reset(timerDuration) case <-mgr.done: if !timer.Stop() { <-timer.C } return false } return true } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { m := zettel.Meta if m.GetBool(meta.KeyNoIndex) { // Zettel maybe in index toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid) mgr.idxCheckZettel(toCheck) return } var cData collectData cData.initialize() collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData) zi := store.NewZettelIndex(m.Zid) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { for _, pair := range m.Pairs(false) { descr := meta.GetDescription(pair.Key) if descr.IsComputed() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi) case meta.TypeIDSet: for _, val := range meta.ListFromValue(pair.Value) { mgr.idxUpdateValue(ctx, descr.Inverse, val, zi) } case meta.TypeZettelmarkup: collectInlineIndexData(parser.ParseMetadata(pair.Value), cData) case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: for _, word := range strfun.NormalizeWords(pair.Value) { cData.words.Add(word) } } } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { if _, err := mgr.GetMeta(ctx, ref); err == nil { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } zi.SetWords(cData.words) zi.SetUrls(cData.urls) } func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } if _, err := mgr.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } zi.AddMetaRef(inverseKey, zid) } func (mgr *Manager) idxDeleteZettel(zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { mgr.idxAr.Enqueue(zid, arUpdate) } } |
Added box/manager/manager.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "log" "net/url" "sort" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/memstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher Notify chan<- box.UpdateInfo } // Connect returns a handle to the specified box. func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) { if authManager.IsReadonly() { rawURL := u.String() // TODO: the following is wrong under some circumstances: // 1. fragment is set if q := u.Query(); len(q) == 0 { rawURL += "?readonly" } else if _, ok := q["readonly"]; !ok { rawURL += "&readonly" } var err error if u, err = url.Parse(rawURL); err != nil { return nil, err } } if create, ok := registry[u.Scheme]; ok { return create(u, cdata) } return nil, &ErrInvalidScheme{u.Scheme} } // ErrInvalidScheme is returned if there is no box with the given scheme. type ErrInvalidScheme struct{ Scheme string } func (err *ErrInvalidScheme) Error() string { return "Invalid scheme: " + err.Scheme } type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error) var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { log.Fatalf("Box with scheme %q already registered", scheme) } registry[scheme] = create } // GetSchemes returns all registered scheme, ordered by scheme string. func GetSchemes() []string { result := make([]string, 0, len(registry)) for scheme := range registry { result = append(result, scheme) } sort.Strings(result) return result } // Manager is a coordinating box. type Manager struct { mgrMx sync.RWMutex started bool rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys map[string]bool // Set of property key names // Indexer data idxStore store.Store idxAr *anterooms idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { propertyKeys := make(map[string]bool) for _, kd := range meta.GetSortedKeyDescriptions() { if kd.IsProperty() { propertyKeys[kd.Name] = true } } mgr := &Manager{ rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxStore: memstore.New(), idxAr: newAnterooms(10), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { return nil, err } if p != nil { boxes = append(boxes, p) cdata.Number++ } } constbox, err := registry[" const"](nil, &cdata) if err != nil { return nil, err } cdata.Number++ compbox, err := registry[" comp"](nil, &cdata) if err != nil { return nil, err } cdata.Number++ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f box.UpdateFunc) { if f != nil { mgr.mxObserver.Lock() mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } func (mgr *Manager) notifyObserver(ci *box.UpdateInfo) { mgr.mxObserver.RLock() observers := mgr.observers mgr.mxObserver.RUnlock() for _, ob := range observers { ob(*ci) } } func (mgr *Manager) notifier() { // The call to notify may panic. Ensure a running notifier. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("Notifier", r) go mgr.notifier() } }() for { select { case ci, ok := <-mgr.infos: if ok { mgr.idxEnqueue(ci.Reason, ci.Zid) if ci.Box == nil { ci.Box = mgr } mgr.notifyObserver(&ci) } case <-mgr.done: return } } } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { case box.OnReload: mgr.idxAr.Reset() case box.OnUpdate: mgr.idxAr.Enqueue(zid, arUpdate) case box.OnDelete: mgr.idxAr.Enqueue(zid, arDelete) default: return } select { case mgr.idxReady <- struct{}{}: default: } } // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() if mgr.started { mgr.mgrMx.Unlock() return box.ErrStarted } for i := len(mgr.boxes) - 1; i >= 0; i-- { ssi, ok := mgr.boxes[i].(box.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } for j := i + 1; j < len(mgr.boxes); j++ { if ssj, ok := mgr.boxes[j].(box.StartStopper); ok { ssj.Stop(ctx) } } mgr.mgrMx.Unlock() return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() go mgr.idxIndexer() // mgr.startIndexer(mgr) mgr.started = true mgr.mgrMx.Unlock() mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} return nil } // Stop the started box. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) error { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if !mgr.started { return box.ErrStopped } close(mgr.done) var err error for _, p := range mgr.boxes { if ss, ok := p.(box.StartStopper); ok { if err1 := ss.Stop(ctx); err1 != nil && err == nil { err = err1 } } } mgr.started = false return err } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() subStats := make([]box.ManagedBoxStats, len(mgr.boxes)) for i, p := range mgr.boxes { p.ReadStats(&subStats[i]) } st.ReadOnly = true sumZettel := 0 for _, sst := range subStats { if !sst.ReadOnly { st.ReadOnly = false } sumZettel += sst.Zettel } st.NumManagedBoxes = len(mgr.boxes) st.ZettelTotal = sumZettel var storeSt store.Stats mgr.idxMx.RLock() defer mgr.idxMx.RUnlock() mgr.idxStore.ReadStats(&storeSt) st.LastReload = mgr.idxLastReload st.IndexesSinceReload = mgr.idxSinceReload st.DurLastReload = mgr.idxDurReload st.ZettelIndexed = storeSt.Zettel st.IndexUpdates = storeSt.Updates st.IndexedWords = storeSt.Words st.IndexedUrls = storeSt.Urls } // Dump internal data structures to a Writer. func (mgr *Manager) Dump(w io.Writer) { mgr.idxStore.Dump(w) } |
Added box/manager/memstore/memstore.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "context" "fmt" "io" "sort" "strings" "sync" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) type metaRefs struct { forward id.Slice backward id.Slice } type zettelIndex struct { dead id.Slice forward id.Slice backward id.Slice meta map[string]metaRefs words []string urls []string } func (zi *zettelIndex) isEmpty() bool { if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { return false } return zi.meta == nil || len(zi.meta) == 0 } type stringRefs map[string]id.Slice type memStore struct { mx sync.RWMutex idx map[id.Zid]*zettelIndex dead map[id.Zid]id.Slice // map dead refs where they occur words stringRefs urls stringRefs // Stats updates uint64 } // New returns a new memory-based index store. func New() store.Store { return &memStore{ idx: make(map[id.Zid]*zettelIndex), dead: make(map[id.Zid]id.Slice), words: make(stringRefs), urls: make(stringRefs), } } func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) { if ms.doEnrich(ctx, m) { ms.mx.Lock() ms.updates++ ms.mx.Unlock() } } func (ms *memStore) doEnrich(ctx context.Context, m *meta.Meta) bool { ms.mx.RLock() defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if len(zi.dead) > 0 { m.Set(meta.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { m.Set(meta.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(meta.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } if len(zi.meta) > 0 { for k, refs := range zi.meta { if len(refs.backward) > 0 { m.Set(k, refs.backward.String()) back = remRefs(back, refs.backward) updated = true } } } if len(back) > 0 { m.Set(meta.KeyBack, back.String()) updated = true } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchEqual(word string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := id.NewSet() if refs, ok := ms.words[word]; ok { result.AddSlice(refs) } if refs, ok := ms.urls[word]; ok { result.AddSlice(refs) } zid, err := id.Parse(word) if err != nil { return result } zi, ok := ms.idx[zid] if !ok { return result } addBackwardZids(result, zid, zi) return result } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchPrefix(prefix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } minZid, err := id.Parse(prefix + "00000000000000"[:14-l]) if err != nil { return result } maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { addBackwardZids(result, zid, zi) } } return result } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchSuffix(suffix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(suffix, strings.HasSuffix) l := len(suffix) if l > 14 { return result } val, err := id.ParseUint(suffix) if err != nil { return result } modulo := uint64(1) for i := 0; i < l; i++ { modulo *= 10 } for zid, zi := range ms.idx { if uint64(zid)%modulo == val { addBackwardZids(result, zid, zi) } } return result } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchContains(s string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(s, strings.Contains) if len(s) > 14 { return result } if _, err := id.ParseUint(s); err != nil { return result } for zid, zi := range ms.idx { if strings.Contains(zid.String(), s) { addBackwardZids(result, zid, zi) } } return result } func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { // Must only be called if ms.mx is read-locked! result := id.NewSet() for word, refs := range ms.words { if !pred(word, s) { continue } result.AddSlice(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } result.AddSlice(refs) } return result } func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is read-locked! result[zid] = true result.AddSlice(zi.backward) for _, mref := range zi.meta { result.AddSlice(mref.backward) } } func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { for _, p := range m.PairsRest(false) { switch meta.Type(p.Key) { case meta.TypeID: if zid, err := id.Parse(p.Value); err == nil { back = remRef(back, zid) } case meta.TypeIDSet: for _, val := range meta.ListFromValue(p.Value) { if zid, err := id.Parse(val); err == nil { back = remRef(back, zid) } } } } return back } func (ms *memStore) UpdateReferences(ctx context.Context, zidx *store.ZettelIndex) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { zi = &zettelIndex{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck id.Set if refs, ok := ms.dead[zidx.Zid]; ok { // These must be checked later again toCheck = id.NewSet(refs...) delete(ms.dead, zidx.Zid) } ms.updateDeadReferences(zidx, zi) ms.updateForwardBackwardReferences(zidx, zi) ms.updateMetadataReferences(zidx, zi) zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) // Check if zi must be inserted into ms.idx if !ziExist && !zi.isEmpty() { ms.idx[zidx.Zid] = zi } return toCheck } func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := refsDiff(drefs, zi.dead) zi.dead = drefs for _, ref := range remRefs { ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) } for _, ref := range newRefs { ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) } } func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := refsDiff(brefs, zi.forward) zi.forward = brefs for _, ref := range remRefs { bzi := ms.getEntry(ref) bzi.backward = remRef(bzi.backward, zidx.Zid) } for _, ref := range newRefs { bzi := ms.getEntry(ref) bzi.backward = addRef(bzi.backward, zidx.Zid) } } func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! metarefs := zidx.GetMetaRefs() for key, mr := range zi.meta { if _, ok := metarefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } if zi.meta == nil { zi.meta = make(map[string]metaRefs) } for key, mrefs := range metarefs { mr := zi.meta[key] newRefs, remRefs := refsDiff(mrefs, mr.forward) mr.forward = mrefs zi.meta[key] = mr for _, ref := range newRefs { bzi := ms.getEntry(ref) if bzi.meta == nil { bzi.meta = make(map[string]metaRefs) } bmr := bzi.meta[key] bmr.backward = addRef(bmr.backward, zidx.Zid) bzi.meta[key] = bmr } ms.removeInverseMeta(zidx.Zid, key, remRefs) } } func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { // Must only be called if ms.mx is write-locked! newWords, removeWords := next.Diff(prev) for _, word := range newWords { if refs, ok := srefs[word]; ok { srefs[word] = addRef(refs, zid) continue } srefs[word] = id.Slice{zid} } for _, word := range removeWords { refs, ok := srefs[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(srefs, word) continue } srefs[word] = refs2 } return next.Words() } func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { // Must only be called if ms.mx is write-locked! if zi, ok := ms.idx[zid]; ok { return zi } zi := &zettelIndex{} ms.idx[zid] = zi return zi } func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ok := ms.idx[zid] if !ok { return nil } ms.deleteDeadSources(zid, zi) toCheck := ms.deleteForwardBackward(zid, zi) if len(zi.meta) > 0 { for key, mrefs := range zi.meta { ms.removeInverseMeta(zid, key, mrefs.forward) } } ms.deleteWords(zid, zi.words) delete(ms.idx, zid) return toCheck } func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! for _, ref := range zi.dead { if drefs, ok := ms.dead[ref]; ok { drefs = remRef(drefs, zid) if len(drefs) > 0 { ms.dead[ref] = drefs } else { delete(ms.dead, ref) } } } } func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set { // Must only be called if ms.mx is write-locked! var toCheck id.Set for _, ref := range zi.forward { if fzi, ok := ms.idx[ref]; ok { fzi.backward = remRef(fzi.backward, zid) } } for _, ref := range zi.backward { if bzi, ok := ms.idx[ref]; ok { bzi.forward = remRef(bzi.forward, zid) if toCheck == nil { toCheck = id.NewSet() } toCheck[ref] = true } } return toCheck } func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { bzi, ok := ms.idx[ref] if !ok || bzi.meta == nil { continue } bmr, ok := bzi.meta[key] if !ok { continue } bmr.backward = remRef(bmr.backward, zid) if len(bmr.backward) > 0 || len(bmr.forward) > 0 { bzi.meta[key] = bmr } else { delete(bzi.meta, key) if len(bzi.meta) == 0 { bzi.meta = nil } } } } func (ms *memStore) deleteWords(zid id.Zid, words []string) { // Must only be called if ms.mx is write-locked! for _, word := range words { refs, ok := ms.words[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(ms.words, word) continue } ms.words[word] = refs2 } } func (ms *memStore) ReadStats(st *store.Stats) { ms.mx.RLock() st.Zettel = len(ms.idx) st.Updates = ms.updates st.Words = uint64(len(ms.words)) st.Urls = uint64(len(ms.urls)) ms.mx.RUnlock() } func (ms *memStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() io.WriteString(w, "=== Dump\n") ms.dumpIndex(w) ms.dumpDead(w) dumpStringRefs(w, "Words", "", "", ms.words) dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) } func (ms *memStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } io.WriteString(w, "==== Zettel Index\n") zids := make(id.Slice, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, "=====", id) zi := ms.idx[id] if len(zi.dead) > 0 { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpZids(w, "* Forward:", zi.forward) dumpZids(w, "* Backward:", zi.backward) for k, fb := range zi.meta { fmt.Fprintln(w, "* Meta", k) dumpZids(w, "** Forward:", fb.forward) dumpZids(w, "** Backward:", fb.backward) } dumpStrings(w, "* Words", "", "", zi.words) dumpStrings(w, "* URLs", "[[", "]]", zi.urls) } } func (ms *memStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } fmt.Fprintf(w, "==== Dead References\n") zids := make(id.Slice, 0, len(ms.dead)) for id := range ms.dead { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, ";", id) fmt.Fprintln(w, ":", ms.dead[id]) } } func dumpZids(w io.Writer, prefix string, zids id.Slice) { if len(zids) > 0 { io.WriteString(w, prefix) for _, zid := range zids { io.WriteString(w, " ") w.Write(zid.Bytes()) } fmt.Fprintln(w) } } func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { if len(slice) > 0 { sl := make([]string, len(slice)) copy(sl, slice) sort.Strings(sl) fmt.Fprintln(w, title) for _, s := range sl { fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) } } } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) slice := make([]string, 0, len(srefs)) for s := range srefs { slice = append(slice, s) } sort.Strings(slice) for _, s := range slice { fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) fmt.Fprintln(w, ":", srefs[s]) } } |
Added box/manager/memstore/refs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import "zettelstore.de/z/domain/id" func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { npos, opos := 0, 0 for npos < len(refsN) && opos < len(refsO) { rn, ro := refsN[npos], refsO[opos] if rn == ro { npos++ opos++ continue } if rn < ro { newRefs = append(newRefs, rn) npos++ continue } remRefs = append(remRefs, ro) opos++ } if npos < len(refsN) { newRefs = append(newRefs, refsN[npos:]...) } if opos < len(refsO) { remRefs = append(remRefs, refsO[opos:]...) } return newRefs, remRefs } func addRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { return refs } else if r < ref { lo = m + 1 } else { hi = m } } refs = append(refs, id.Invalid) copy(refs[hi+1:], refs[hi:]) refs[hi] = ref return refs } func remRefs(refs, rem id.Slice) id.Slice { if len(refs) == 0 || len(rem) == 0 { return refs } result := make(id.Slice, 0, len(refs)) rpos, dpos := 0, 0 for rpos < len(refs) && dpos < len(rem) { rr, dr := refs[rpos], rem[dpos] if rr < dr { result = append(result, rr) rpos++ continue } if dr < rr { dpos++ continue } rpos++ dpos++ } if rpos < len(refs) { result = append(result, refs[rpos:]...) } return result } func remRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { copy(refs[m:], refs[m+1:]) refs = refs[:len(refs)-1] return refs } else if r < ref { lo = m + 1 } else { hi = m } } return refs } |
Added box/manager/memstore/refs_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "testing" "zettelstore.de/z/domain/id" ) func assertRefs(t *testing.T, i int, got, exp id.Slice) { t.Helper() if got == nil && exp != nil { t.Errorf("%d: got nil, but expected %v", i, exp) return } if got != nil && exp == nil { t.Errorf("%d: expected nil, but got %v", i, got) return } if len(got) != len(exp) { t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) return } for p, n := range exp { if got := got[p]; got != id.Zid(n) { t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) } } } func TestRefsDiff(t *testing.T) { t.Parallel() testcases := []struct { in1, in2 id.Slice exp1, exp2 id.Slice }{ {nil, nil, nil, nil}, {id.Slice{1}, nil, id.Slice{1}, nil}, {nil, id.Slice{1}, nil, id.Slice{1}}, {id.Slice{1}, id.Slice{1}, nil, nil}, {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, } for i, tc := range testcases { got1, got2 := refsDiff(tc.in1, tc.in2) assertRefs(t, i, got1, tc.exp1) assertRefs(t, i, got2, tc.exp2) } } func TestAddRef(t *testing.T) { t.Parallel() testcases := []struct { ref id.Slice zid uint exp id.Slice }{ {nil, 5, id.Slice{5}}, {id.Slice{1}, 5, id.Slice{1, 5}}, {id.Slice{10}, 5, id.Slice{5, 10}}, {id.Slice{5}, 5, id.Slice{5}}, {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, } for i, tc := range testcases { got := addRef(tc.ref, id.Zid(tc.zid)) assertRefs(t, i, got, tc.exp) } } func TestRemRefs(t *testing.T) { t.Parallel() testcases := []struct { in1, in2 id.Slice exp id.Slice }{ {nil, nil, nil}, {nil, id.Slice{}, nil}, {id.Slice{}, nil, id.Slice{}}, {id.Slice{}, id.Slice{}, id.Slice{}}, {id.Slice{1}, id.Slice{5}, id.Slice{1}}, {id.Slice{10}, id.Slice{5}, id.Slice{10}}, {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, } for i, tc := range testcases { got := remRefs(tc.in1, tc.in2) assertRefs(t, i, got, tc.exp) } } func TestRemRef(t *testing.T) { t.Parallel() testcases := []struct { ref id.Slice zid uint exp id.Slice }{ {nil, 5, nil}, {id.Slice{}, 5, id.Slice{}}, {id.Slice{5}, 5, id.Slice{}}, {id.Slice{1}, 5, id.Slice{1}}, {id.Slice{10}, 5, id.Slice{10}}, {id.Slice{1, 5}, 5, id.Slice{1}}, {id.Slice{5, 10}, 5, id.Slice{10}}, {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, } for i, tc := range testcases { got := remRef(tc.ref, id.Zid(tc.zid)) assertRefs(t, i, got, tc.exp) } } |
Added box/manager/store/store.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store import ( "context" "io" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. Zettel int // Updates count the number of metadata updates. Updates uint64 // Words count the different words stored in the store. Words uint64 // Urls count the different URLs stored in the store. Urls uint64 } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { search.Searcher // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) id.Set // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) id.Set // ReadStats populates st with store statistics. ReadStats(st *Stats) // Dump the content to a Writer. Dump(io.Writer) } |
Added box/manager/store/wordset.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store // WordSet contains the set of all words, with the count of their occurrences. type WordSet map[string]int // NewWordSet returns a new WordSet. func NewWordSet() WordSet { return make(WordSet) } // Add one word to the set func (ws WordSet) Add(s string) { ws[s] = ws[s] + 1 } // Words gives the slice of all words in the set. func (ws WordSet) Words() []string { if len(ws) == 0 { return nil } words := make([]string, 0, len(ws)) for w := range ws { words = append(words, w) } return words } // Diff calculates the word slice to be added and to be removed from oldWords // to get the given word set. func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) { if len(ws) == 0 { return nil, oldWords } if len(oldWords) == 0 { return ws.Words(), nil } oldSet := make(WordSet, len(oldWords)) for _, ow := range oldWords { if _, ok := ws[ow]; ok { oldSet[ow] = 1 continue } removeWords = append(removeWords, ow) } for w := range ws { if _, ok := oldSet[w]; ok { continue } newWords = append(newWords, w) } return newWords, removeWords } |
Added box/manager/store/wordset_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store_test import ( "sort" "testing" "zettelstore.de/z/box/manager/store" ) func equalWordList(exp, got []string) bool { if len(exp) != len(got) { return false } if len(got) == 0 { return len(exp) == 0 } sort.Strings(got) for i, w := range exp { if w != got[i] { return false } } return true } func TestWordsWords(t *testing.T) { t.Parallel() testcases := []struct { words store.WordSet exp []string }{ {nil, nil}, {store.WordSet{}, nil}, {store.WordSet{"a": 1, "b": 2}, []string{"a", "b"}}, } for i, tc := range testcases { got := tc.words.Words() if !equalWordList(tc.exp, got) { t.Errorf("%d: %v.Words() == %v, but got %v", i, tc.words, tc.exp, got) } } } func TestWordsDiff(t *testing.T) { t.Parallel() testcases := []struct { cur store.WordSet old []string expN, expR []string }{ {nil, nil, nil, nil}, {store.WordSet{}, []string{}, nil, nil}, {store.WordSet{"a": 1}, []string{}, []string{"a"}, nil}, {store.WordSet{"a": 1}, []string{"b"}, []string{"a"}, []string{"b"}}, {store.WordSet{}, []string{"b"}, nil, []string{"b"}}, {store.WordSet{"a": 1}, []string{"a"}, nil, nil}, } for i, tc := range testcases { gotN, gotR := tc.cur.Diff(tc.old) if !equalWordList(tc.expN, gotN) { t.Errorf("%d: %v.Diff(%v)->new %v, but got %v", i, tc.cur, tc.old, tc.expN, gotN) } if !equalWordList(tc.expR, gotR) { t.Errorf("%d: %v.Diff(%v)->rem %v, but got %v", i, tc.cur, tc.old, tc.expR, gotR) } } } |
Added box/manager/store/zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store import "zettelstore.de/z/domain/id" // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { Zid id.Zid // zid of the indexed zettel backrefs id.Set // set of back references metarefs map[string]id.Set // references to inverse keys deadrefs id.Set // set of dead references words WordSet urls WordSet } // NewZettelIndex creates a new zettel index. func NewZettelIndex(zid id.Zid) *ZettelIndex { return &ZettelIndex{ Zid: zid, backrefs: id.NewSet(), metarefs: make(map[string]id.Set), deadrefs: id.NewSet(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs[zid] = true } // AddMetaRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { if zids, ok := zi.metarefs[key]; ok { zids[zid] = true return } zi.metarefs[key] = id.NewSet(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { zi.deadrefs[zid] = true } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } // GetBackRefs returns all back references as a sorted list. func (zi *ZettelIndex) GetBackRefs() id.Slice { return zi.backrefs.Sorted() } // GetMetaRefs returns all meta references as a map of strings to a sorted list of references func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice { if len(zi.metarefs) == 0 { return nil } result := make(map[string]id.Slice, len(zi.metarefs)) for key, refs := range zi.metarefs { result[key] = refs.Sorted() } return result } // GetWords returns a reference to the set of words. It must not be modified. func (zi *ZettelIndex) GetWords() WordSet { return zi.words } // GetUrls returns a reference to the set of URLs. It must not be modified. func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls } |
Added box/membox/membox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox import ( "context" "net/url" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &memBox{u: u, cdata: *cdata}, nil }) } type memBox struct { u *url.URL cdata manager.ConnectData zettel map[id.Zid]domain.Zettel mx sync.RWMutex } func (mp *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { if chci := mp.cdata.Notify; chci != nil { chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } func (mp *memBox) Location() string { return mp.u.String() } func (mp *memBox) Start(ctx context.Context) error { mp.mx.Lock() mp.zettel = make(map[id.Zid]domain.Zettel) mp.mx.Unlock() return nil } func (mp *memBox) Stop(ctx context.Context) error { mp.mx.Lock() mp.zettel = nil mp.mx.Unlock() return nil } func (mp *memBox) CanCreateZettel(ctx context.Context) bool { return true } func (mp *memBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mp.mx.Lock() zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := mp.zettel[zid] return !ok, nil }) if err != nil { mp.mx.Unlock() return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta mp.zettel[zid] = zettel mp.mx.Unlock() mp.notifyChanged(box.OnUpdate, zid) return zid, nil } func (mp *memBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mp.mx.RLock() zettel, ok := mp.zettel[zid] mp.mx.RUnlock() if !ok { return domain.Zettel{}, box.ErrNotFound } zettel.Meta = zettel.Meta.Clone() return zettel, nil } func (mp *memBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mp.mx.RLock() zettel, ok := mp.zettel[zid] mp.mx.RUnlock() if !ok { return nil, box.ErrNotFound } return zettel.Meta.Clone(), nil } func (mp *memBox) FetchZids(ctx context.Context) (id.Set, error) { mp.mx.RLock() result := id.NewSetCap(len(mp.zettel)) for zid := range mp.zettel { result[zid] = true } mp.mx.RUnlock() return result, nil } func (mp *memBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) { result := make([]*meta.Meta, 0, len(mp.zettel)) mp.mx.RLock() for _, zettel := range mp.zettel { m := zettel.Meta.Clone() mp.cdata.Enricher.Enrich(ctx, m, mp.cdata.Number) if match(m) { result = append(result, m) } } mp.mx.RUnlock() return result, nil } func (mp *memBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return true } func (mp *memBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mp.mx.Lock() meta := zettel.Meta.Clone() if !meta.Zid.IsValid() { return &box.ErrInvalidID{Zid: meta.Zid} } zettel.Meta = meta mp.zettel[meta.Zid] = zettel mp.mx.Unlock() mp.notifyChanged(box.OnUpdate, meta.Zid) return nil } func (mp *memBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true } func (mp *memBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mp.mx.Lock() zettel, ok := mp.zettel[curZid] if !ok { mp.mx.Unlock() return box.ErrNotFound } // Check that there is no zettel with newZid if _, ok = mp.zettel[newZid]; ok { mp.mx.Unlock() return &box.ErrInvalidID{Zid: newZid} } meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mp.zettel[newZid] = zettel delete(mp.zettel, curZid) mp.mx.Unlock() mp.notifyChanged(box.OnDelete, curZid) mp.notifyChanged(box.OnUpdate, newZid) return nil } func (mp *memBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { mp.mx.RLock() _, ok := mp.zettel[zid] mp.mx.RUnlock() return ok } func (mp *memBox) DeleteZettel(ctx context.Context, zid id.Zid) error { mp.mx.Lock() if _, ok := mp.zettel[zid]; !ok { mp.mx.Unlock() return box.ErrNotFound } delete(mp.zettel, zid) mp.mx.Unlock() mp.notifyChanged(box.OnDelete, zid) return nil } func (mp *memBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = false mp.mx.RLock() st.Zettel = len(mp.zettel) mp.mx.RUnlock() } |
Added box/merge.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box import "zettelstore.de/z/domain/meta" // MergeSorted returns a merged sequence of metadata, sorted by Zid. // The lists first and second must be sorted descending by Zid. func MergeSorted(first, second []*meta.Meta) []*meta.Meta { lenFirst := len(first) lenSecond := len(second) result := make([]*meta.Meta, 0, lenFirst+lenSecond) iFirst := 0 iSecond := 0 for iFirst < lenFirst && iSecond < lenSecond { zidFirst := first[iFirst].Zid zidSecond := second[iSecond].Zid if zidFirst > zidSecond { result = append(result, first[iFirst]) iFirst++ } else if zidFirst < zidSecond { result = append(result, second[iSecond]) iSecond++ } else { // zidFirst == zidSecond result = append(result, first[iFirst]) iFirst++ iSecond++ } } if iFirst < lenFirst { result = append(result, first[iFirst:]...) } else { result = append(result, second[iSecond:]...) } return result } |
Added client/client.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/url" "strconv" "strings" "time" "zettelstore.de/z/api" "zettelstore.de/z/domain/id" ) // Client contains all data to execute requests. type Client struct { baseURL string username string password string token string tokenType string expires time.Time } // NewClient create a new client. func NewClient(baseURL string) *Client { if !strings.HasSuffix(baseURL, "/") { baseURL += "/" } c := Client{baseURL: baseURL} return &c } func (c *Client) newURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(c.baseURL, key) } func (c *Client) newRequest(ctx context.Context, method string, ub *api.URLBuilder, body io.Reader) (*http.Request, error) { return http.NewRequestWithContext(ctx, method, ub.String(), body) } func (c *Client) executeRequest(req *http.Request) (*http.Response, error) { if c.token != "" { req.Header.Add("Authorization", c.tokenType+" "+c.token) } client := http.Client{} resp, err := client.Do(req) if err != nil { if resp != nil && resp.Body != nil { resp.Body.Close() } return nil, err } return resp, err } func (c *Client) buildAndExecuteRequest( ctx context.Context, method string, ub *api.URLBuilder, body io.Reader, h http.Header) (*http.Response, error) { req, err := c.newRequest(ctx, method, ub, body) if err != nil { return nil, err } err = c.updateToken(ctx) if err != nil { return nil, err } for key, val := range h { req.Header[key] = append(req.Header[key], val...) } return c.executeRequest(req) } // SetAuth sets authentication data. func (c *Client) SetAuth(username, password string) { c.username = username c.password = password c.token = "" c.tokenType = "" c.expires = time.Time{} } func (c *Client) executeAuthRequest(req *http.Request) error { resp, err := c.executeRequest(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var tinfo api.AuthJSON err = dec.Decode(&tinfo) if err != nil { return err } c.token = tinfo.Token c.tokenType = tinfo.Type c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second) return nil } func (c *Client) updateToken(ctx context.Context) error { if c.username == "" { return nil } if time.Now().After(c.expires) { return c.Authenticate(ctx) } return c.RefreshToken(ctx) } // Authenticate sets a new token by sending user name and password. func (c *Client) Authenticate(ctx context.Context) error { authData := url.Values{"username": {c.username}, "password": {c.password}} req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('v'), strings.NewReader(authData.Encode())) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return c.executeAuthRequest(req) } // RefreshToken updates the access token func (c *Client) RefreshToken(ctx context.Context) error { req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('v'), nil) if err != nil { return err } return c.executeAuthRequest(req) } // CreateZettel creates a new zettel and returns its URL. func (c *Client) CreateZettel(ctx context.Context, data *api.ZettelDataJSON) (id.Zid, error) { var buf bytes.Buffer if err := encodeZettelData(&buf, data); err != nil { return id.Invalid, err } ub := c.jsonZettelURLBuilder(nil) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) if err != nil { return id.Invalid, err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return id.Invalid, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var newZid api.ZidJSON err = dec.Decode(&newZid) if err != nil { return id.Invalid, err } zid, err := id.Parse(newZid.ID) if err != nil { return id.Invalid, err } return zid, nil } func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error { enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) return enc.Encode(&data) } // ListZettel returns a list of all Zettel. func (c *Client) ListZettel(ctx context.Context, query url.Values) ([]api.ZettelJSON, error) { ub := c.jsonZettelURLBuilder(query) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var zl api.ZettelListJSON err = dec.Decode(&zl) if err != nil { return nil, err } return zl.List, nil } // GetZettelJSON returns a zettel as a JSON struct. func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid, query url.Values) (*api.ZettelDataJSON, error) { ub := c.jsonZettelURLBuilder(query).SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZettelDataJSON err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // GetEvaluatedZettel return a zettel in a defined encoding. func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) { ub := c.jsonZettelURLBuilder(nil).SetZid(zid) ub.AppendQuery(api.QueryKeyFormat, enc.String()) ub.AppendQuery(api.QueryKeyPart, api.PartContent) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", errors.New(resp.Status) } content, err := io.ReadAll(resp.Body) if err != nil { return "", err } return string(content), nil } // GetZettelOrder returns metadata of the given zettel and, more important, // metadata of zettel that are referenced in a list within the first zettel. func (c *Client) GetZettelOrder(ctx context.Context, zid id.Zid) (*api.ZidMetaRelatedList, error) { ub := c.newURLBuilder('o').SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZidMetaRelatedList err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // ContextDirection specifies how the context should be calculated. type ContextDirection uint8 // Allowed values for ContextDirection const ( _ ContextDirection = iota DirBoth DirBackward DirForward ) // GetZettelContext returns metadata of the given zettel and, more important, // metadata of zettel that for the context of the first zettel. func (c *Client) GetZettelContext( ctx context.Context, zid id.Zid, dir ContextDirection, depth, limit int) ( *api.ZidMetaRelatedList, error, ) { ub := c.newURLBuilder('x').SetZid(zid) switch dir { case DirBackward: ub.AppendQuery(api.QueryKeyDir, api.DirBackward) case DirForward: ub.AppendQuery(api.QueryKeyDir, api.DirForward) } if depth > 0 { ub.AppendQuery(api.QueryKeyDepth, strconv.Itoa(depth)) } if limit > 0 { ub.AppendQuery(api.QueryKeyLimit, strconv.Itoa(limit)) } resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZidMetaRelatedList err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // GetZettelLinks returns connections to ohter zettel, images, externals URLs. func (c *Client) GetZettelLinks(ctx context.Context, zid id.Zid) (*api.ZettelLinksJSON, error) { ub := c.newURLBuilder('l').SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZettelLinksJSON err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // UpdateZettel updates an existing zettel. func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data *api.ZettelDataJSON) error { var buf bytes.Buffer if err := encodeZettelData(&buf, data); err != nil { return err } ub := c.jsonZettelURLBuilder(nil).SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { return errors.New(resp.Status) } return nil } // RenameZettel renames a zettel. func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid id.Zid) error { ub := c.jsonZettelURLBuilder(nil).SetZid(oldZid) h := http.Header{ api.HeaderDestination: {c.jsonZettelURLBuilder(nil).SetZid(newZid).String()}, } resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { return errors.New(resp.Status) } return nil } // DeleteZettel deletes a zettel with the given identifier. func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error { ub := c.jsonZettelURLBuilder(nil).SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { return errors.New(resp.Status) } return nil } func (c *Client) jsonZettelURLBuilder(query url.Values) *api.URLBuilder { ub := c.newURLBuilder('z') for key, values := range query { if key == api.QueryKeyFormat { continue } for _, val := range values { ub.AppendQuery(key, val) } } return ub } // ListTags returns a map of all tags, together with the associated zettel containing this tag. func (c *Client) ListTags(ctx context.Context) (map[string][]string, error) { err := c.updateToken(ctx) if err != nil { return nil, err } req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('t'), nil) if err != nil { return nil, err } resp, err := c.executeRequest(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var tl api.TagListJSON err = dec.Decode(&tl) if err != nil { return nil, err } return tl.Tags, nil } // ListRoles returns a list of all roles. func (c *Client) ListRoles(ctx context.Context) ([]string, error) { err := c.updateToken(ctx) if err != nil { return nil, err } req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('r'), nil) if err != nil { return nil, err } resp, err := c.executeRequest(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var rl api.RoleListJSON err = dec.Decode(&rl) if err != nil { return nil, err } return rl.Roles, nil } |
Added client/client_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client_test import ( "context" "flag" "fmt" "net/url" "testing" "zettelstore.de/z/api" "zettelstore.de/z/client" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestCreateRenameDeleteZettel(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("creator", "creator") zid, err := c.CreateZettel(context.Background(), &api.ZettelDataJSON{ Meta: nil, Encoding: "", Content: "Example", }) if err != nil { t.Error("Cannot create zettel:", err) return } if !zid.IsValid() { t.Error("Invalid zettel ID", zid) return } newZid := zid + 1 c.SetAuth("owner", "owner") err = c.RenameZettel(context.Background(), zid, newZid) if err != nil { t.Error("Cannot rename", zid, ":", err) newZid = zid } err = c.DeleteZettel(context.Background(), newZid) if err != nil { t.Error("Cannot delete", zid, ":", err) return } } func TestUpdateZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("writer", "writer") z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) if err != nil { t.Error(err) return } if got := z.Meta[meta.KeyTitle]; got != "Home" { t.Errorf("Title of zettel is not \"Home\", but %q", got) return } newTitle := "New Home" z.Meta[meta.KeyTitle] = newTitle err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, z) if err != nil { t.Error(err) return } zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) if err != nil { t.Error(err) return } if got := zt.Meta[meta.KeyTitle]; got != newTitle { t.Errorf("Title of zettel is not %q, but %q", newTitle, got) } } func TestList(t *testing.T) { testdata := []struct { user string exp int }{ {"", 7}, {"creator", 10}, {"reader", 12}, {"writer", 12}, {"owner", 34}, } t.Parallel() c := getClient() query := url.Values{api.QueryKeyFormat: {"html"}} // Client must remove "html" for i, tc := range testdata { t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { c.SetAuth(tc.user, tc.user) l, err := c.ListZettel(context.Background(), query) if err != nil { tt.Error(err) return } got := len(l) if got != tc.exp { tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } l, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}}) if err != nil { t.Error(err) return } got := len(l) if got != 27 { t.Errorf("List of length %d expected, but got %d\n%v", 27, got, l) } } func TestGetZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, url.Values{api.QueryKeyPart: {api.PartContent}}) if err != nil { t.Error(err) return } if m := z.Meta; len(m) > 0 { t.Errorf("Exptected empty meta, but got %v", z.Meta) } if z.Content == "" || z.Encoding != "" { t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) } } func TestGetEvaluatedZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") encodings := []api.EncodingEnum{ api.EncoderDJSON, api.EncoderHTML, api.EncoderNative, api.EncoderText, } for _, enc := range encodings { content, err := c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc) if err != nil { t.Error(err) continue } if len(content) == 0 { t.Errorf("Empty content for encoding %v", enc) } } } func TestGetZettelOrder(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelOrder(context.Background(), id.TOCNewTemplateZid) if err != nil { t.Error(err) return } if rl.ID != id.TOCNewTemplateZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.TOCNewTemplateZid, rl.ID) return } l := rl.List if got := len(l); got != 2 { t.Errorf("Expected list fo length 2, got %d", got) return } if got := l[0].ID; got != id.TemplateNewZettelZid.String() { t.Errorf("Expected result[0]=%v, but got %v", id.TemplateNewZettelZid, got) } if got := l[1].ID; got != id.TemplateNewUserZid.String() { t.Errorf("Expected result[1]=%v, but got %v", id.TemplateNewUserZid, got) } } func TestGetZettelContext(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelContext(context.Background(), id.VersionZid, client.DirBoth, 0, 3) if err != nil { t.Error(err) return } if rl.ID != id.VersionZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) return } l := rl.List if got := len(l); got != 3 { t.Errorf("Expected list fo length 3, got %d", got) return } if got := l[0].ID; got != id.DefaultHomeZid.String() { t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) } if got := l[1].ID; got != id.OperatingSystemZid.String() { t.Errorf("Expected result[1]=%v, but got %v", id.OperatingSystemZid, got) } if got := l[2].ID; got != id.StartupConfigurationZid.String() { t.Errorf("Expected result[2]=%v, but got %v", id.StartupConfigurationZid, got) } rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0) if err != nil { t.Error(err) return } if rl.ID != id.VersionZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) return } l = rl.List if got := len(l); got != 1 { t.Errorf("Expected list fo length 1, got %d", got) return } if got := l[0].ID; got != id.DefaultHomeZid.String() { t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) } } func TestGetZettelLinks(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") zl, err := c.GetZettelLinks(context.Background(), id.DefaultHomeZid) if err != nil { t.Error(err) return } if zl.ID != id.DefaultHomeZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.DefaultHomeZid, zl.ID) return } if len(zl.Links.Incoming) != 0 { t.Error("No incomings expected", zl.Links.Incoming) } if got := len(zl.Links.Outgoing); got != 4 { t.Errorf("Expected 4 outgoing links, got %d", got) } if got := len(zl.Links.Local); got != 1 { t.Errorf("Expected 1 local link, got %d", got) } if got := len(zl.Links.External); got != 4 { t.Errorf("Expected 4 external link, got %d", got) } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") tm, err := c.ListTags(context.Background()) if err != nil { t.Error(err) return } tags := []struct { key string size int }{ {"#invisible", 1}, {"#user", 4}, {"#test", 4}, } if len(tm) != len(tags) { t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm) } for _, tag := range tags { if zl, ok := tm[tag.key]; !ok { t.Errorf("No tag %v: %v", tag.key, tm) } else if len(zl) != tag.size { t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) } } for i, id := range tm["#user"] { if id != tm["#test"][i] { t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"]) } } } func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.ListRoles(context.Background()) if err != nil { t.Error(err) return } exp := []string{"configuration", "user", "zettel"} if len(rl) != len(exp) { t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl) } for i, id := range exp { if id != rl[i] { t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i]) } } } var baseURL string func init() { flag.StringVar(&baseURL, "base-url", "", "Base URL") } func getClient() *client.Client { return client.NewClient(baseURL) } // TestMain controls whether client API tests should run or not. func TestMain(m *testing.M) { flag.Parse() if baseURL != "" { m.Run() } } |
Changes to cmd/cmd_file.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import ( "flag" "fmt" "io" "os" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "flag" "fmt" "io" "os" "zettelstore.de/z/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) |
︙ | ︙ | |||
36 37 38 39 40 41 42 | domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), nil, ) | | | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), nil, ) enc := encoder.Create(api.Encoder(format), &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)}) if enc == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", format) return 2, nil } _, err = enc.WriteZettel(os.Stdout, z, format != "raw") if err != nil { return 2, err |
︙ | ︙ |
Changes to cmd/cmd_run.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 | package cmd import ( "flag" "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" | > > < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package cmd import ( "flag" "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" ) // ---------- Subcommand: run ------------------------------------------------ |
︙ | ︙ | |||
54 55 56 57 58 59 60 | kern.SetDebug(debug) if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } | | | | | | | > | | | | | > > > | > > | < < < | < | < | < | | > > | > > | | > > > > > | > | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | kern.SetDebug(debug) if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig) api := api.New(webSrv, authManager, authManager, webSrv, rtConfig) wui := webui.New(webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, boxManager) ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucListMeta := usecase.NewListMeta(protectedBoxManager) ucListRoles := usecase.NewListRole(protectedBoxManager) ucListTags := usecase.NewListTags(protectedBoxManager) ucZettelContext := usecase.NewZettelContext(protectedBoxManager) ucDelete := usecase.NewDeleteZettel(protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(protectedBoxManager) ucRename := usecase.NewRenameZettel(protectedBoxManager) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) // Web user interface webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler()) webSrv.AddListRoute('a', http.MethodPost, wui.MakePostLoginHandler(ucAuthenticate)) webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler()) if !authManager.IsReadonly() { webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler(ucRename)) webSrv.AddZettelRoute('c', http.MethodGet, wui.MakeGetCopyZettelHandler( ucGetZettel, usecase.NewCopyZettel())) webSrv.AddZettelRoute('c', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('d', http.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler(ucDelete)) webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(ucUpdate)) webSrv.AddZettelRoute('f', http.MethodGet, wui.MakeGetFolgeZettelHandler( ucGetZettel, usecase.NewFolgeZettel(rtConfig))) webSrv.AddZettelRoute('f', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('g', http.MethodGet, wui.MakeGetNewZettelHandler( ucGetZettel, usecase.NewNewZettel())) webSrv.AddZettelRoute('g', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) } webSrv.AddListRoute('f', http.MethodGet, wui.MakeSearchHandler( usecase.NewSearch(protectedBoxManager), ucGetMeta, ucGetZettel)) webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler( ucListMeta, ucListRoles, ucListTags)) webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler( ucParseZettel, ucGetMeta)) webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler( ucParseZettel, ucGetMeta, ucGetAllMeta)) webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext)) // API webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( usecase.NewZettelOrder(protectedBoxManager, ucParseZettel))) webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) webSrv.AddListRoute('v', http.MethodPost, api.MakePostLoginHandler(ucAuthenticate)) webSrv.AddListRoute('v', http.MethodPut, api.MakeRenewAuthHandler()) webSrv.AddZettelRoute('x', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( usecase.NewListMeta(protectedBoxManager), ucGetMeta, ucParseZettel)) webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( ucParseZettel, ucGetMeta)) if !authManager.IsReadonly() { webSrv.AddListRoute('z', http.MethodPost, api.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('z', http.MethodDelete, api.MakeDeleteZettelHandler(ucDelete)) webSrv.AddZettelRoute('z', http.MethodPut, api.MakeUpdateZettelHandler(ucUpdate)) webSrv.AddZettelRoute('z', zsapi.MethodMove, api.MakeRenameZettelHandler(ucRename)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } |
Changes to cmd/command.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "sort" "zettelstore.de/z/domain/meta" ) // Command stores information about commands / sub-commands. type Command struct { | | | | | > | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "sort" "zettelstore.de/z/domain/meta" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command Boxes bool // if true then boxes will be set up Header bool // Print a heading on startup LineServer bool // Start admin line server Flags func(*flag.FlagSet) // function to set up flag.FlagSet flags *flag.FlagSet // flags that belong to the command } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error) |
︙ | ︙ |
Changes to cmd/fd_limit_raise.go.
︙ | ︙ | |||
37 38 39 40 41 42 43 | return err } err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } if rLimit.Cur < minFiles { | | | 37 38 39 40 41 42 43 44 45 46 47 | return err } err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } if rLimit.Cur < minFiles { log.Printf("Make sure you have no more than %d files in all your boxes if you enabled notification\n", rLimit.Cur) } return nil } |
Changes to cmd/main.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 25 26 27 28 29 | "net/url" "os" "strconv" "strings" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" | > > > < < < | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "net/url" "os" "strconv" "strings" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) const ( defConfigfile = ".zscfg" ) |
︙ | ︙ | |||
50 51 52 53 54 55 56 | }) RegisterCommand(Command{ Name: "version", Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ | | | | | > | | | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | }) RegisterCommand(Command{ Name: "version", Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ Name: "run", Func: runFunc, Boxes: true, Header: true, LineServer: true, Flags: flgRun, }) RegisterCommand(Command{ Name: "run-simple", Func: runSimpleFunc, Boxes: true, Header: true, Flags: flgSimpleRun, }) RegisterCommand(Command{ Name: "file", Func: cmdFile, Flags: func(fs *flag.FlagSet) { |
︙ | ︙ | |||
109 110 111 112 113 114 115 | case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } | | | | | | | | | | | | | | | | | | | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } cfg.Set(keyBoxOneURI, val) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) return cfg } func parsePort(s string) (string, error) { port, err := net.LookupPort("tcp", s) if err != nil { fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s) return "", err } return strconv.Itoa(port), nil } const ( keyAdminPort = "admin-port" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyListenAddr = "listen-addr" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose" ) func setServiceConfig(cfg *meta.Meta) error { ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val) } ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) ok = setConfigValue( ok, kernel.BoxService, kernel.BoxDefaultDirType, cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify)) ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") format := kernel.BoxURIs + "%v" for i := 1; ; i++ { key := fmt.Sprintf(format, i) val, found := cfg.Get(key) if !found { break } ok = setConfigValue(ok, kernel.BoxService, key, val) } ok = setConfigValue( ok, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) |
︙ | ︙ | |||
191 192 193 194 195 196 197 | done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) if !done { kernel.Main.Log("unable to set configuration:", key, val) } return ok && done } | | | | | | | | | | | 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) if !done { kernel.Main.Log("unable to set configuration:", key, val) } return ok && done } func setupOperations(cfg *meta.Meta, withBoxes bool) { var createManager kernel.CreateBoxManagerFunc if withBoxes { err := raiseFdLimit() if err != nil { srvm := kernel.Main srvm.Log("Raising some limitions did not work:", err) srvm.Log("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") srvm.SetConfig(kernel.BoxService, kernel.BoxDefaultDirType, kernel.BoxDirTypeSimple) } createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { compbox.Setup(cfg) return manager.New(boxURIs, authManager, rtConfig) } } else { createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil } } kernel.Main.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil }, createManager, func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) } func executeCommand(name string, args ...string) int { |
︙ | ︙ | |||
237 238 239 240 241 242 243 | return 1 } cfg := getConfig(fs) if err := setServiceConfig(cfg); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } | | | | 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | return 1 } cfg := getConfig(fs) if err := setServiceConfig(cfg); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } setupOperations(cfg, command.Boxes) kernel.Main.Start(command.Header, command.LineServer) exitCode, err := command.Func(fs, cfg) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kernel.Main.Shutdown(true) return exitCode } |
︙ | ︙ |
Changes to cmd/register.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd // Mention all needed encoders, parsers and stores to have them registered. import ( _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/jsonenc" // Allow to use JSON encoder. _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. _ "zettelstore.de/z/encoder/rawenc" // Allow to use raw encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. | > > > > > < < < < < | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd // Mention all needed encoders, parsers and stores to have them registered. import ( _ "zettelstore.de/z/box/compbox" // Allow to use computed box. _ "zettelstore.de/z/box/constbox" // Allow to use global internal box. _ "zettelstore.de/z/box/dirbox" // Allow to use directory box. _ "zettelstore.de/z/box/filebox" // Allow to use file box. _ "zettelstore.de/z/box/membox" // Allow to use in-memory box. _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/jsonenc" // Allow to use JSON encoder. _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. _ "zettelstore.de/z/encoder/rawenc" // Allow to use raw encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) |
Changes to collect/collect.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect | < | < | < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < | < < | < | | < < < | | | | < < < | | | | < < | < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // Summary stores the relevant parts of the syntax tree type Summary struct { Links []*ast.Reference // list of all referenced links Images []*ast.Reference // list of all referenced images Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) (s Summary) { ast.WalkBlockSlice(&s, zn.Ast) return s } // Visit all node to collect data for the summary. func (s *Summary) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.LinkNode: s.Links = append(s.Links, n.Ref) case *ast.ImageNode: if n.Ref != nil { s.Images = append(s.Images, n.Ref) } case *ast.CiteNode: s.Cites = append(s.Cites, n) } return s } |
Changes to collect/collect_test.go.
︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 33 34 35 36 | if !r.IsValid() { panic(s) } return r } func TestLinks(t *testing.T) { zn := &ast.ZettelNode{} summary := collect.References(zn) if summary.Links != nil || summary.Images != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Images) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} | > | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | if !r.IsValid() { panic(s) } return r } func TestLinks(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{} summary := collect.References(zn) if summary.Links != nil || summary.Images != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Images) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} |
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 60 61 62 63 | summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestImage(t *testing.T) { zn := &ast.ZettelNode{ Ast: ast.BlockSlice{ &ast.ParaNode{ Inlines: ast.InlineSlice{ &ast.ImageNode{Ref: parseRef("12345678901234")}, }, }, | > | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestImage(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{ Ast: ast.BlockSlice{ &ast.ParaNode{ Inlines: ast.InlineSlice{ &ast.ImageNode{Ref: parseRef("12345678901234")}, }, }, |
︙ | ︙ |
Changes to collect/order.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import "zettelstore.de/z/ast" // Order of internal reference within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { if ln, ok := bn.(*ast.NestedListNode); ok { | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import "zettelstore.de/z/ast" // Order of internal reference within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { if ln, ok := bn.(*ast.NestedListNode); ok { switch ln.Kind { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { if ref := firstItemZettelReference(is); ref != nil { result = append(result, ref) } } } |
︙ | ︙ |
Changes to config/config.go.
︙ | ︙ | |||
52 53 54 55 56 57 58 | // GetMarkerExternal returns the current value of the "marker-external" key. GetMarkerExternal() string // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. GetFooterHTML() string | < < < < | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | // GetMarkerExternal returns the current value of the "marker-external" key. GetMarkerExternal() string // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. GetFooterHTML() string } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // GetExpertMode returns the current value of the "expert-mode" key GetExpertMode() bool |
︙ | ︙ |
Changes to docs/manual/00001002000000.zettel.
︙ | ︙ | |||
11 12 13 14 15 16 17 | : It should be not hard to write other software that works with your zettel. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If your device is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | : It should be not hard to write other software that works with your zettel. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If your device is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation : If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working. : Upgrading the software is done just by replacing the executable with a newer one. ; Ease of operation : There is only one executable for Zettelstore and one directory, where your zettel are stored. : If you decide to use multiple directories, you are free to configure Zettelstore appropriately. ; Multiple modes of operation : You can use Zettelstore as a standalone software on your device, but you are not restricted to it. : You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel. ; Multiple user interfaces : Zettelstore provides a default web-based user interface. Anybody can provide alternative user interfaces, e.g. for special purposes. ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. |
Changes to docs/manual/00001003000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it into any directory * Start the Zettelstore software, e.g. with a double click | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it into any directory * Start the Zettelstore software, e.g. with a double click * A sub-directory ""zettel"" will be created in the directory where you put the executable. It will contain your future zettel. * Open the URI [[http://localhost:23123]] with your web browser. It will present you a mostly empty Zettelstore. There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information. * Please read the instructions for the web-based user interface and learn about the various ways to write zettel. * If you restart your device, please make sure to start your Zettelstore again. |
︙ | ︙ | |||
39 40 41 42 43 44 45 | ```sh # sudo useradd --system --gid zettelstore \ --create-home --home-dir /var/lib/zettelstore \ --shell /usr/sbin/nologin \ --comment "Zettelstore server" \ zettelstore ``` | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | ```sh # sudo useradd --system --gid zettelstore \ --create-home --home-dir /var/lib/zettelstore \ --shell /usr/sbin/nologin \ --comment "Zettelstore server" \ zettelstore ``` Create a systemd service file and store it into ''/etc/systemd/system/zettelstore.service'': ```ini [Unit] Description=Zettelstore After=network.target [Service] Type=simple |
︙ | ︙ |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | | > > > > > > > > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210712234656 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. An attacker that is able to change the owner can do anything. Therefore only the owner of the computer on which Zettelstore runs can change this information. The file for startup configuration must be created via a text editor in advance. The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: ; [!admin-port]''admin-port'' : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. A value of ''0'' (the default) disables the administrator console. The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). Default: ''0'' ; [!box-uri-x]''box-uri-//X//'', where //X// is a number greater or equal to one : Specifies a [[box|00001004011200]] where zettel are stored. During startup //X// is counted up, starting with one, until no key is found. This allows to configure more than one box. If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ''dir://.zettel''. In this case, even a key ''box-uri-2'' will be ignored. ; [!default-dir-box-type]''default-dir-box-type'' : Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]]. Zettel are typically stored in such boxes. Default: ''notify'' ; [!insecure-cookie]''insecure-cookie'' : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. Default: ''false'' |
︙ | ︙ | |||
48 49 50 51 52 53 54 | On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. If ''true'', a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ''false'' | < < < < < < < | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. If ''true'', a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ''false'' ; [!read-only-mode]''read-only-mode'' : Puts the Zettelstore web service into a read-only mode. No changes are possible. Default: false. ; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html'' : Define lifetime of access tokens in minutes. Values are only valid if authentication is enabled, i.e. key ''owner'' is set. |
︙ | ︙ | |||
78 79 80 81 82 83 84 | Must begin and end with a slash character (""''/''"", ''U+002F''). Default: ''"/"''. This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; ''verbose'' : Be more verbose inf logging data. Default: false | < < | 79 80 81 82 83 84 85 | Must begin and end with a slash character (""''/''"", ''U+002F''). Default: ''"/"''. This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; ''verbose'' : Be more verbose inf logging data. Default: false |
Changes to docs/manual/00001004011200.zettel.
1 | id: 00001004011200 | | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525121452 A Zettelstore must store its zettel somehow and somewhere. In most cases you want to store your zettel as files in a directory. Under certain circumstances you may want to store your zettel elsewhere. An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore. They are stored within the software itself. In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting. To cope with these (and more) situations, you configure Zettelstore to use one or more //boxes//{-}. This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number). Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. The following box URIs are supported: ; ''dir:\//DIR'' : Specifies a directory where zettel files are stored. ''DIR'' is the file path. Although it is possible to use relative file paths, such as ''./zettel'' (→ URI is ''dir:\//.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''. The directory must exist before starting the Zettelstore[^There is one exception: when Zettelstore is [[started without any parameter|00001004050000]], e.g. via double-clicking its icon, an directory called ''./zettel'' will be created.]. It is possible to [[configure|00001004011400]] a directory box. ; ''file:FILE.zip'' oder ''file:/\//path/to/file.zip'' : Specifies a ZIP file which contains files that store zettel. You can create such a ZIP file, if you zip a directory full of zettel files. This box is always read-only. ; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. All boxes that you configure via the ''box-uri-X'' keys form a chain of boxes. If a zettel should be retrieved, a search starts in the box specified with the ''box-uri-2'' key, then ''box-uri-3'' and so on. If a zettel is created or changed, it is always stored in the box specified with the ''box-uri-1'' key. This allows to overwrite zettel from other boxes, e.g. the predefined zettel. If you use the ''mem:'' box, where zettel are stored in volatile memory, it makes only sense if you configure it as ''box-uri-1''. Such a box will be empty when Zettelstore starts and only the first box will receive updates. You must make sure that your computer has enough RAM to store all zettel. |
Changes to docs/manual/00001004011400.zettel.
1 | id: 00001004011400 | | | | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | id: 00001004011400 title: Configure file directory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525121232 Under certain circumstances, it is preferable to further configure a file directory box. This is done by appending query parameters after the base box URI ''dir:\//DIR''. The following parameters are supported: |= Parameter:|Description|Default value:| |type|(Sub-) Type of the directory service|(value of ''[[default-dir-box-type|00001004010000#default-dir-box-type]]'') |rescan|Time (in seconds) after which the directory should be scanned fully|600 |worker|Number of worker that can access the directory in parallel|(depends on type) |readonly|Allow only operations that do not change a zettel or create a new zettel|n/a === Type On some operating systems, Zettelstore tries to detect changes to zettel files outside of Zettelstore's control[^This includes Linux, Windows, and macOS.]. On other operating systems, this may be not possible, due to technical limitations. Automatic detection of external changes is also not possible, if zettel files are put on an external service, such as a file server accessed via SMD/CIFS or NFS. To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box. The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual. The following values are supported: ; simple : Is not able to detect external changes. Works on all platforms. Is a little slower than other implementations (up to three times slower). ; notify : Automatically detect external changes. Tries to optimize performance, at a little cost of main memory (RAM). === Rescan When the parameter ''type'' is set to ""notify"", Zettelstore automatically detects changes to zettel files that originates from other software. It is done on a ""best-effort"" basis. Under certain circumstances it is possible that Zettelstore does not detect a change done by another software. To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file directory. The time interval is configured by the ''rescan'' parameter, e.g. ``` box-uri-1: dir:///home/zettel?rescan=300 ``` This makes Zettelstore to re-scan the directory ''/home/zettel/'' every 300 seconds, i.e. 5 minutes. For a Zettelstore with many zettel, re-scanning the directory may take a while, especially if it is stored on a remote computer (e.g. via CIFS/SMB or NFS). In this case, you should adjust the parameter value. Please note that a directory re-scan invalidates all internal data of a Zettelstore. It might trigger a re-build of the backlink database (and other internal databases). Therefore a large value is preferred. This value is ignored for other directory box types, such as ""simple"". === Worker Internally, Zettelstore parallels concurrent requests for a zettel or its metadata. The number of parallel activities is configured by the ''worker'' parameter. A computer contains a limited number of internal processing units (CPU). Its number ranges from 1 to (currently) 128, e.g. in bigger server environments. Zettelstore typically runs on a system with 1 to 8 CPUs. Access to zettel file is ultimately managed by the underlying operating system. Depending on the hardware and on the type of the directory box, only a limited number of parallel accesses are desirable. On smaller hardware[^In comparison to a normal desktop or laptop computer], such as the [[Raspberry Zero|https://www.raspberrypi.org/products/raspberry-pi-zero/]], a smaller value might be appropriate. Every worker needs some amount of main memory (RAM) and some amount of processing power. On bigger hardware, with some fast file services, a bigger value could result in higher performance, if needed. For a directory box of type ""notify"", the default value is: 7. The directory box type ""simple"" limits the value to a maximum of 1, i.e. no concurrency is possible with this type of directory box. For various reasons, the value should be a prime number, with a maximum value of 1499. === Readonly Sometimes you may want to provide zettel from a file directory box, but you want to disallow any changes. If you provide the query parameter ''readonly'' (with or without a corresponding value), the box will disallow any changes. ``` box-uri-1: dir:///home/zettel?readonly ``` If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory boxes will be in read-only mode too, even if not explicitly configured. |
Changes to docs/manual/00001004020000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]]. This zettel is called ""configuration zettel"". The following metadata keys change the appearance / behavior of Zettelstore: ; [!default-copyright]''default-copyright'' : Copyright value to be used when rendering content. | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210611213730 You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]]. This zettel is called ""configuration zettel"". The following metadata keys change the appearance / behavior of Zettelstore: ; [!default-copyright]''default-copyright'' : Copyright value to be used when rendering content. |
︙ | ︙ | |||
50 51 52 53 54 55 56 | Default: (the empty string). ; [!home-zettel]''home-zettel'' : Specifies the identifier of the zettel, that should be presented for the default view / home view. If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown. ; [!marker-external]''marker-external'' : Some HTML code that is displayed after a reference to external material. Default: ''&\#10138;'', to display a ""➚"" sign. | < < < < | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | Default: (the empty string). ; [!home-zettel]''home-zettel'' : Specifies the identifier of the zettel, that should be presented for the default view / home view. If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown. ; [!marker-external]''marker-external'' : Some HTML code that is displayed after a reference to external material. Default: ''&\#10138;'', to display a ""➚"" sign. ; [!site-name]''site-name'' : Name of the Zettelstore instance. Will be used when displaying some lists. Default: ''Zettelstore''. ; [!yaml-header]''yaml-header'' : If true, metadata and content will be separated by ''-\--\\n'' instead of an empty line (''\\n\\n''). Default: ''false''. |
︙ | ︙ |
Changes to docs/manual/00001004050200.zettel.
1 2 3 4 5 | id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712233414 Lists all implemented sub-commands. Example: ``` # zettelstore help Available commands: - "file" - "help" - "password" - "run" - "run-simple" - "version" ``` |
Changes to docs/manual/00001004050400.zettel.
1 2 3 4 5 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | | | < | | | > | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234031 Emits some information about the Zettelstore's version. This allows you to check, whether your installed Zettelstore is The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer. The build version information is a string like ''1.0.2+351ae138b4''. The part ""1.0.2"" is the release version. ""+351ae138b4"" is a code uniquely identifying the version to the developer. Everything after the release version is optional, eg. ""1.4.3"" is a valid build version information too. Example: ``` # zettelstore version Zettelstore 1.0.2+351ae138b4 (go1.16.5@linux/amd64) Licensed under the latest version of the EUPL (European Union Public License) ``` In this example, Zettelstore is running in the released version ""1.0.2"" and was compiled using [[Go, version 1.16.5|https://golang.org/doc/go1.16]]. The software was build for running under a Linux operating system with an ""amd64"" processor. |
Changes to docs/manual/00001004051000.zettel.
1 2 3 4 5 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234419 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v] ``` ; [!a]''-a PORT'' : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details. ; [!c]''-c CONFIGFILE'' : Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read. |
︙ | ︙ |
Changes to docs/manual/00001004051100.zettel.
1 2 3 4 5 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234203 === ``zettelstore run-simple`` This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon. It is s simplified variant of the [[''run'' sub-command|00001004051000]]. It allows only to specify a zettel directory. The directory will be created automatically, if it does not exist. This is a difference to the ''run'' sub-command, where the directory must exists. In contrast to the ''run'' sub-command, other command line parameter are not allowed. ``` zettelstore run-simple [-d DIR] ``` ; [!d]''-d DIR'' : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". |
Changes to docs/manual/00001004051200.zettel.
1 2 3 4 5 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234222 Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout. This allows Zettelstore to render files manually. ``` zettelstore file [-t FORMAT] [file-1 [file-2]] ``` |
︙ | ︙ |
Changes to docs/manual/00001004051400.zettel.
1 2 3 4 5 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234305 This sub-command is used to create a hashed password for to be authenticated users. It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output. The general usage is: ``` zettelstore password IDENT ZETTEL-ID ``` ``IDENT`` is the identification for the user that should be authenticated. ``ZETTEL-ID`` is the [[identifier of the zettel|00001006050000]] that later acts as a user zettel. See [[Creating an user zettel|00001010040200]] for some background information. An example: ``` # zettelstore password bob 20200911115600 Password: Again: |
︙ | ︙ |
Changes to docs/manual/00001004101000.zettel.
︙ | ︙ | |||
56 57 58 59 60 61 62 | : Displays s list of all available services and their current status. ; ''set-config SERVICE KEY VALUE'' : Sets a single configuration value for the next configuration of a given service. It will become effective if the service is restarted. If the key specifies a list value, all other list values with a number greater than the given key are deleted. You can use the special number ""0"" to delete all values. | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | : Displays s list of all available services and their current status. ; ''set-config SERVICE KEY VALUE'' : Sets a single configuration value for the next configuration of a given service. It will become effective if the service is restarted. If the key specifies a list value, all other list values with a number greater than the given key are deleted. You can use the special number ""0"" to delete all values. E.g. ``set-config box box-uri-0 any_text`` will remove all values of the list //box-uri-//. ; ''shutdown'' : Terminate the Zettelstore itself (and closes the connection to the administrator console). ; ''start SERVICE'' : Start the given bservice and all dependent services. ; ''stat SERVICE'' : Display some statistical values for the given service. ; ''stop SERVICE'' : Stop the given service and all other that depend on this. |
Changes to docs/manual/00001005000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk Zettelstore is a software that manages your zettel. Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories. Typically, file names and file content must comply to specific rules so that Zettelstore can manage them. If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions. Zettelstore provides additional services to the user. Via a builtin web interface you can work with zettel in various ways. For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" (API) that allows other software to communicate with the Zettelstore. Zettelstore becomes extensible by external software. For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. === Where zettel are stored | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20210614165848 Zettelstore is a software that manages your zettel. Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories. Typically, file names and file content must comply to specific rules so that Zettelstore can manage them. If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions. Zettelstore provides additional services to the user. Via a builtin web interface you can work with zettel in various ways. For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" (API) that allows other software to communicate with the Zettelstore. Zettelstore becomes extensible by external software. For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. === Where zettel are stored Your zettel are stored typically as files in a specific directory. If you have not explicitly specified the directory, a default directory will be used. The directory has to be specified at [[startup time|00001004010000]]. Nested directories are not supported (yet). Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss''). This allows zettel to be sorted naturally by creation time. |
︙ | ︙ | |||
44 45 46 47 48 49 50 | For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores. The solution is a ''.meta'' file with the same zettel identifier. Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure. It maintains this relationship as long as theses files exists. In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files. | | | | > > > > > > > > > > > > | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores. The solution is a ''.meta'' file with the same zettel identifier. Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure. It maintains this relationship as long as theses files exists. In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files. Here the ''.zettel'' extension will signal that the metadata and the zettel content will be put in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator""). === Predefined zettel Zettelstore contains some [[predefined zettel|00001005090000]] to work properly. The [[configuration zettel|00001004020000]] is one example. To render the builtin web interface, some templates are used, as well as a layout specification in CSS. The icon that visualizes a broken image is a predefined GIF image. All of these are visible to the Zettelstore as zettel. One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences. Where are these zettel stored? They are stored within the Zettelstore software itself, because one design goal was to have just one executable file to use Zettelstore. But data stored within an executable programm cannot be changed later[^Well, it can, but it is a very bad idea to allow this. Mostly for security reasons.]. To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together. If you change a zettel, it will be always stored as a file. If a zettel is requested, Zettelstore will first try to read that zettel from a file. If such a file was not found, the internal zettel store is searched secondly. Therefore, the file store ""shadows"" the internal zettel store. If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier. Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[List of predefined zettel|00001005090000]] === Boxes: other ways to store zettel As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself. Zettelstore allows other ways to store zettel by providing an abstraction called //box//.[^Formerly, zettel were stored physically in boxes, often made of wood.] A file directory which stores zettel is called a ""directory box"". But zettel may be also stored in a ZIP file, which is called ""file box"". For testing purposes, zettel may be stored in volatile memeory (called //RAM//). This way is called ""memory box"". Other types of boxes could be added to Zettelstore. What about a ""remote Zettelstore box""? |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk | | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk modified: 20210622124647 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore | [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore | [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore | [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore | [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content | [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Templöate | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles | [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000040001]] | Generic Emoji | Image that is shown if original image reference is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]"" | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. ; [!back]''back'' | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20210709162756 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. ; [!back]''back'' |
︙ | ︙ | |||
44 45 46 47 48 49 50 51 52 53 54 55 | : Date and time when a zettel was modified through Zettelstore. If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. ; [!no-index]''no-index'' : If set to true, the zettel will not be indexed and therefore not be found in full-text searches. ; [!precursor]''precursor'' : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!published]''published'' : This property contains the timestamp of the mast modification / creation of the zettel. | > > > | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | : Date and time when a zettel was modified through Zettelstore. If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. ; [!no-index]''no-index'' : If set to true, the zettel will not be indexed and therefore not be found in full-text searches. ; [!box-number]''box-number'' : Is a computed value and contains the number of the box where the zettel was found. For all but the [[predefined zettel|00001005090000]], this number is equal to the number //X// specified in startup configuration key [[''box-uri-//X//''|00001004010000#box-uri-x]]. ; [!precursor]''precursor'' : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!published]''published'' : This property contains the timestamp of the mast modification / creation of the zettel. If [[''modified''|#modified]] is set, it contains the same value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. It can be used for [[sorting|00001012052000]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. ; [!read-only]''read-only'' : Marks a zettel as read-only. The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not. ; [!role]''role'' |
︙ | ︙ |
Changes to docs/manual/00001006020400.zettel.
︙ | ︙ | |||
12 13 14 15 16 17 18 | === No authentication If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is something else (the value ""true"" is recommended), the user cannot modify the zettel through the web interface. | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | === No authentication If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is something else (the value ""true"" is recommended), the user cannot modify the zettel through the web interface. However, if the zettel is stored as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor. === Authentication enabled If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is the same as an explicit [[user role|00001010070300]], users with that role (or a role with lower rights) are not allowed to modify the zettel. ; ""reader"" : Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel. Users with role ""writer"" or the owner itself still can modify the zettel. ; ""writer"" : Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel. Only the owner of the Zettelstore can modify the zettel. If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended), no user is allowed modify the zettel through the web interface. However, if the zettel is accessible as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor. Typically the owner of a Zettelstore have such an access. |
Changes to docs/manual/00001006030000.zettel.
1 2 3 4 5 6 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk | > | < | > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20210627170437 All [[supported metadata keys|00001006020000]] conform to a type. User-defined metadata keys conform also to a type, based on the suffix of the key. |=Suffix|Type | ''-number'' | [[Number|00001006033000]] | ''-url'' | [[URL|00001006035000]] | ''-zid'' | [[Identifier|00001006032000]] | any other suffix | [[EString|00001006031500]] The name of the metadata key is bound to the key type Every key type has an associated validation rule to check values of the given type. There is also a rule how values are matched, e.g. against a search term when selecting some zettel. And there is a rule, how values compare for sorting. * [[Boolean|00001006030500]] |
︙ | ︙ |
Changes to docs/manual/00001006050000.zettel.
1 2 3 4 | id: 00001006050000 title: Zettel identifier tags: #design #manual #zettelstore syntax: zmk | > | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | id: 00001006050000 title: Zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20210721123222 Each zettel is given a unique identifier. To some degree, the zettel identifier is part of the metadata. Basically, the identifier is given by the [[Zettelstore|00001005000000]] software. Every zettel identifier consists of 14 digits. They resemble a timestamp: the first four digits could represent the year, the next two represent the month, following by day, hour, minute, and second. This allows to order zettel chronologically in a canonical way. In most cases the zettel identifier is the timestamp when the zettel was created. However, the Zettelstore software just checks for exactly 14 digits. Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with a month part of ""35"" or with ""99"" as the last two digits. Some zettel identifier are [[reserved|00001006055000]] and should not be used otherwise. All identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''. Zettel identifier of this manual have be chosen to begin with ""000010"". A zettel can have any identifier that contains 14 digits and that is not in use by another zettel managed by the same Zettelstore. |
Added docs/manual/00001006055000.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20210721125518 [[Zettel identifier|00001006050000]] are typically created by examine the current date and time. By renaming a zettel, you are able to provide any sequence of 14 digits. If no other zettel has the same identifier, you are allowed to rename a zettel. To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000''). All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.]. Zettel identifier of this manual have be chosen to begin with ''000010''. However, some external applications may need a range of specific zettel identifier to work properly. Identifier that begin with ''00009'' can be used for such purpose. To request a reservation, please send an email to the maintainer of Zettelstore. The request must include the following data: ; Title : Title of you application ; Description : A brief description what the application is used for and why you need to reserve some zettel identifier ; Number : Specify the amount of zettel identifier you are planning to use. Minimum size is 100. If you need more than 10.000, your justification will contain more words. === Reserved Zettel Identifier |= From | To | Description | 00000000000000 | 0000000000000 | This is an invalid zettel identifier | 00000000000001 | 0000009999999 | [[Predefined zettel|00001005090000]] | 00000100000000 | 0000019999999 | Zettelstore manual | 00000200000000 | 0000899999999 | Reserved for future use | 00009000000000 | 0000999999999 | Reserved for applications This list may change in the future. ==== External Applications |= From | To | Description | 00009000001000 | 00009000001000 | ZS Slides, an application to display zettel as a HTML-based slideshow |
Changes to docs/manual/00001007010000.zettel.
︙ | ︙ | |||
20 21 22 23 24 25 26 | Inline elements mostly begins with two non-space, often identical characters. With some exceptions, two identical non-space characters begins a formatting range that is ended with the same two characters. Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"". A link is given with ``[[...]]``{=zmk}, an images with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}. An inline comment, beginning with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins. | | | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | Inline elements mostly begins with two non-space, often identical characters. With some exceptions, two identical non-space characters begins a formatting range that is ended with the same two characters. Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"". A link is given with ``[[...]]``{=zmk}, an images with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}. An inline comment, beginning with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins. The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If put at the end of non-space text.]. Some inline elements do not follow the rule of two identical character, especially to specify footnotes, citation keys, and local marks. These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``""). One inline element that does not begin with two characters is the ""entity"". It allows to specify any Unicode character. The specification of that character is put between an ampersand character and a semicolon: ``&...;``{=zmk}. For exmple, an ""n-dash"" could also be specified as ``–``{==zmk}. The backslash character (""``\\``"") possibly gives the next character a special meaning. This allows to resolve some left ambiguities. For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}. An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}. To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified. Many block and inline elements can be refined by additional attributes. Attributes resemble roughly HTML attributes and are put near the corresponding elements by using the syntax ``{...}``{=zmk}. One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``. To summarize: * With some exceptions, blocks-structural elements begins at the for position of a line with three identical characters. * The most important exception to this rule is the specification of lists. * If no block element is found, a paragraph with inline elements is assumed. |
︙ | ︙ |
Changes to docs/manual/00001007030200.zettel.
︙ | ︙ | |||
86 87 88 89 90 91 92 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. Instead you should put a horizonal rule (""thematic break"") between them. You could also use a mark element or a hard line break to separate the two lists: ```zmk # One # Two [!sep] # Uno # Due |
︙ | ︙ |
Changes to docs/manual/00001007031000.zettel.
︙ | ︙ | |||
89 90 91 92 93 94 95 | |123|123|123|123 ::: === Rows to be ignored A line that begins with the sequence ''|%'' (vertical bar character (""''|''"", ''U+007C''), followed by a percent sign character (“%”, U+0025)) will be ignored. This allows to specify a horizontal rule that is not rendered. Such tables are emitted by some commands of the [[administrator console|00001004100000]]. | | | | | | | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |123|123|123|123 ::: === Rows to be ignored A line that begins with the sequence ''|%'' (vertical bar character (""''|''"", ''U+007C''), followed by a percent sign character (“%”, U+0025)) will be ignored. This allows to specify a horizontal rule that is not rendered. Such tables are emitted by some commands of the [[administrator console|00001004100000]]. For example, the command ``get-config box`` will emit ``` |=Key | Value | Description |%-----------+--------+--------------------------- | defdirtype | notify | Default directory box type ``` This is rendered in HTML as: :::example |=Key | Value | Description |%-----------+--------+--------------------------- | defdirtype | notify | Default directory box type ::: |
Changes to docs/manual/00001007040000.zettel.
︙ | ︙ | |||
43 44 45 46 47 48 49 | They will be considered equivalent to tags in metadata. ==== Entities & more Sometimes it is not easy to enter special characters. If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name. Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B''). | | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | They will be considered equivalent to tags in metadata. ==== Entities & more Sometimes it is not easy to enter special characters. If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name. Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B''). If you know the HTML name of the character you want to enter, put it between these two character. Example: ``&`` is rendered as ::&::{=example}. If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10. Example: ``&`` is rendered in HTML as ::&::{=example}. You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character. Example: ``&`` is rendered in HTML as ::&::{=example}. Since some Unicode character are used quite often, a special notation is introduced for them: * Two consecutive hyphen-minus characters result in an //en-dash// character. It is typically used in numeric ranges. ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}. Alternative specifications are: ``–``, ``&x8211``, and ``–``. * Three consecutive full stop characters (""''.''"", ''U+002E'') after a space result in an horizontal ellipsis character. ``to be continued ... later`` will be rendered in HTML as: ::to be continued, ... later::{=example}. Alternative specifications are: ``…``, ``&x8230``, and ``…``. |
Changes to docs/manual/00001008000000.zettel.
1 2 3 4 5 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk modified: 20210705111758 [[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content. Zettelstore is quite agnostic with respect to markup languages. Of course, Zettelmarkup plays an important role. However, with the exception of zettel titles, you can use any (markup) language that is supported: * CSS |
︙ | ︙ | |||
21 22 23 24 25 26 27 28 29 30 31 32 | The following syntax values are supported: ; [!css]''css'' : A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML. ; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png'' : The formats for pixel graphics. Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file. ; [!markdown]''markdown'', [!md]''md'' : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|https://commonmark.org/]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. ; [!mustache]''mustache'' | > > > > > > | | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | The following syntax values are supported: ; [!css]''css'' : A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML. ; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png'' : The formats for pixel graphics. Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file. ; [!html]''html'' : Hypertext Markup Language, will not be parsed further. Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]). For security reasons, equivocal elements will not be encoded in the HTML format / web user interface, e.g. the ``<script ...`` tag. See [[security aspects of Markdown|00001008010000#security-aspects]] for some details. ; [!markdown]''markdown'', [!md]''md'' : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|https://commonmark.org/]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. ; [!mustache]''mustache'' : A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]]. ; [!none]''none'' : Only the metadata of a zettel is ""parsed"". Useful for displaying the full metadata. The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. ; [!svg]''svg'' : A [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. |
︙ | ︙ |
Changes to docs/manual/00001010070300.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001010070300 title: User roles role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk Every user is associated with some basic privileges. These are specified in the user zettel with the key ''user-role''. The following values are supported: | > | | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | id: 00001010070300 title: User roles role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20210702165501 Every user is associated with some basic privileges. These are specified in the user zettel with the key ''user-role''. The following values are supported: ; [!reader]""reader"" : The user is allowed to read zettel. This is the default value for any user except the owner of the Zettelstore. ; [!writer]""writer"" : The user is allowed to create new zettel and to change existing zettel. ; [!creator]""creator"" : The user is only allowed to create new zettel. It is also allowed to change its own user zettel. There are two other user roles, implicitly defined: ; The anonymous user : This role is assigned to any user that is not authenticated. Can only read zettel with visibility [[public|00001010070200]], but cannot change them. ; The owner : The user that is configured to be the owner of the Zettelstore. Does not need to specify a user role in its user zettel. Is not restricted in the use of Zettelstore, except when a zettel is marked as [[read-only|00001006020400]]. |
Changes to docs/manual/00001010070600.zettel.
1 2 3 4 | id: 00001010070600 title: Access rules tags: #authorization #configuration #manual #security #zettelstore syntax: zmk | > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | id: 00001010070600 title: Access rules role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20210702165416 Whether an operation of the Zettelstore is allowed or rejected, depends on various factors. The following rules are checked first, in this order: # In read-only mode, every operation except the ""Read"" operation is rejected. # If there is no owner, authentication is disabled and every operation is allowed for everybody. # If the user is authenticated and it is the owner, then the operation is allowed. In the second step, when authentication is enabled and the requesting user is not the owner, everything depends on the requested operation. * Read a zettel: ** If the visibility is ""public"", the access is granted. ** If the visibility is ""owner"", the access is rejected. ** If the user is not authenticated, access is rejected. ** If the zettel requested is an user zettel, reject the access if the users identification is not the same as of the ''ident'' meta key in the zettel. In other words: only the requesting user is allowed to access its own user zettel. ** If the ''user-role'' of the user is ""creator"", reject the access. ** Otherwise the user is authenticated, no sensitive zettel is requested. Allow to read the zettel. * Create a new zettel ** If the user is not authenticated, reject the access. ** If the ''user-role'' of the user is ""reader"", reject the access. ** If the user tries to create an user zettel, the access is rejected. |
︙ | ︙ |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. === Background The API is HTTP-based and uses JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721120820 The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. === Background The API is HTTP-based and uses JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. |
︙ | ︙ | |||
26 27 28 29 30 31 32 | * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[List metadata of all zettel|00001012051200]] * [[List all zettel, but in different encoding formats|00001012051400]] * [[List all zettel, but include different parts of a zettel|00001012051600]] | | > > > > | | | | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[List metadata of all zettel|00001012051200]] * [[List all zettel, but in different encoding formats|00001012051400]] * [[List all zettel, but include different parts of a zettel|00001012051600]] * [[Shape the list of zettel metadata|00001012051800]] ** [[Selection of zettel|00001012051810]] ** [[Zettel parts|00001012051820]] ** [[Limit the list length|00001012051830]] ** [[Content search|00001012051840]] ** [[Sort the list of zettel metadata|00001012052000]] * [[List all tags|00001012052200]] * [[List all roles|00001012052400]] === Working with zettel * [[Create a new zettel|00001012053200]] * [[Retrieve metadata and content of an existing zettel|00001012053400]] * [[Retrieve references of an existing zettel|00001012053600]] * [[Retrieve context of an existing zettel|00001012053800]] * [[Retrieve zettel order within an existing zettel|00001012054000]] * [[Update metadata and content of a zettel|00001012054200]] * [[Rename a zettel|00001012054400]] * [[Delete a zettel|00001012054600]] |
Changes to docs/manual/00001012050200.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[startup configuration|00001004010000]] (typically 10 minutes). | > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210712221945 Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[startup configuration|00001004010000]] (typically 10 minutes). The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/v''[^The endpoint ''/a'' is already taken for the [[web user interface|00001014000000]], ""v"" stands for the German word ""verbürgen""] with a POST request: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL: ```sh # curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data: ```sh # curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` In all cases, you will receive an JSON object will all [[relevant data|00001012921000]] to be used for further API calls. **Important:** obtaining a token is a time-intensive process. Zettelstore will delay every request to obtain a token for a certain amount of time. |
︙ | ︙ |
Changes to docs/manual/00001012050400.zettel.
1 2 3 4 5 6 7 8 9 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk An access token is only valid for a certain duration. Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data. | > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210712222135 An access token is only valid for a certain duration. Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data. Send a HTTP PUT request to the [[endpoint|00001012920000]] ''/v'' and include the current access token in the ''Authorization'' header: ```sh # curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456} ``` You may receive a new access token, or the current one if it was obtained not a long time ago. However, the lifetime of the returned [[access token|00001012921000]] is accurate. === HTTP Status codes ; ''200'' |
︙ | ︙ |
Changes to docs/manual/00001012051800.zettel.
1 | id: 00001012051800 | | | < < < < < | < < < < < | < < < < < < < < < < | < < < < < < < < < < < < < < < < | < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012051800 title: API: Shape the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721120658 In most cases, it is not essential to list //all// zettel. Typically, you are interested only in a subset of the zettel maintained by your Zettelstore. This is done by adding some query parameters to the general ''GET /z'' request. * [[Select|00001012051810]] just some zettel, based on metadata. * You can specify which [[parts of a zettel|00001012051820]] must be returned. * Only a specific amount of zettel will be selected by specifying [[a length and/or an offset|00001012051830]]. * [[Searching for specific content|00001012051840]], not just the metadata, is another way of selecting some zettel. * The resulting list can be [[sorted|00001012052000]] according to various criteria. |
Added docs/manual/00001012051810.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | id: 00001012051810 title: API: Select zettel based on their metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721121038 Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key. According to the [[type|00001006030000]] of a metadata key, zettel are possibly selected. All [[supported|00001006020000]] metadata keys have a well-defined type. User-defined keys have the type ''e'' (string, possibly empty). For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=API' {"list":[{"id":"00001012921000","url":"/z/00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","url":"/z/00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","url":"/z/00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... ``` However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021''). For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=!API' {"list":[{"id":"00010000000000","url":"/z/00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","url":"/z/00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, ... ``` In both cases, an implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true. But the situation is different for a key like [[''url''|00001006020000#url]]. Both ``curl 'http://localhost:23123/z?url='`` and ``curl 'http://localhost:23123/z?url=!'`` may result in an empty list. The empty query parameter values matches all zettel that contain the given metadata key. Similar, if you specify just the exclamation mark character as a query parameter value, only those zettel match that does //not// contain the given metadata key. This is in contrast to above rule that the metadata value must exist before a match is done. For example ``curl 'http://localhost:23123/z?back=!&backward='`` returns all zettel that are reachable via other zettel, but also references these zettel. Above example shows that all sub-expressions of a select specification must be true so that no zettel is rejected from the final list. If you specify the query parameter ''_negate'', either with or without a value, the whole selection will be negated. Because of the precondition described above, ``curl 'http://127.0.0.1:23123/z?url=!com'`` and ``curl 'http://127.0.0.1:23123/z?url=com&_negate'`` may produce different lists. The first query produces a zettel list, where each zettel does have a ''url'' metadata value, which does not contain the characters ""com"". The second query produces a zettel list, that excludes any zettel containing a ''url'' metadata value that contains the characters ""com""; this also includes all zettel that do not contain the metadata key ''url''. |
Added docs/manual/00001012051820.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | id: 00001012051820 title: API: Shape the list of zettel metadata by returning specific parts of a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721120743 === Basic usage If you are just interested in the zettel identifier, you should add the [[''_part''|00001012920800]] query parameter: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id' {"list":[{"id":"00001012921000","url":"/z/00001012921000"},{"id":"00001012920500","url":"/z/00001012920500"},{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001012051800","url":"/z/00001012051800"},{"id":"00001012051600","url":"/z/00001012051600"},{"id":"00001012051400","url":"/z/00001012051400"},{"id":"00001012051200","url":"/z/00001012051200"},{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012000000","url":"/z/00001012000000"}]} ``` === Combined usage with select options If you want only those zettel that additionally must contain the string ""JSON"", you have to add an additional query parameter: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id&title=JSON' {"list":[{"id":"00001012921000","url":"/z/00001012921000"}]} ``` Similarly, if you add another query parameter, the intersection of both results is returned: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id&id=00001012050' {"list":[{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"}]} ``` |
Added docs/manual/00001012051830.zettel.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001012051830 title: API: Shape the list of zettel metadata by limiting its length role: manual tags: #api #manual #zettelstore syntax: zmk === Limit By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements: ```sh # curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2' {"list":[{"id":"00001012000000","url":"/z/00001012000000"},{"id":"00001012050200","url":"/z/00001012050200"}]} ``` === Offset The query parameter ""''_offset''"" allows to list not only the first elements, but to begin at a specific element: ```sh # curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2&_offset=1' {"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]} ``` |
Added docs/manual/00001012051840.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | id: 00001012051840 title: API: Shape the list of zettel metadata by searching the content role: manual tags: #api #manual #zettelstore syntax: zmk The query parameter ""''_s''"" allows to provide a string for a full-text search of all zettel. The search string will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. If the search string starts with the character ""''!''"", it will be removed and the query matches all zettel that **do not match** the search string. In the next step, the first character of the search string will be inspected. If it contains one of the characters ""'':''"", ""''=''"", ""''>''"", or ""''~''"", this will modify how the search will be performed. The character will be removed from the start of the search string. For example, assume the search string is ""def"": ; ""'':''"", ""''~''"" (or none of these characters)[^""'':''"" is always the character for specifying the default comparison. In this case, it is equal to ""''~''"". If you omit a comparison character, the default comparison is used.] : The zettel must contain a word that contains the search string. ""def"", ""defghi"", and ""abcdefghi"" are matching the search string. ; ""''=''"" : The zettel must contain a word that is equal to the search string. Only the word ""def"" matches the search string. ; ""''>''"" : The zettel must contain a word with the search string as a prefix. A word like ""def"" or ""defghi"" matches the search string. If you want to include an initial ""''!''"" into the search string, you must prefix that with the escape character ""''\\''"". For example ""\\!abc"" will search for zettel that contains the string ""!abc"". A similar rule applies to the characters that specify the way how the search will be done. For example, ""!\\=abc"" will search for zettel that do not contains the string ""=abc"". You are allowed to specify this query parameter more than once. All results will be intersected, i.e. a zettel will be included into the list if all of the provided values match. This parameter loosely resembles the search box of the web user interface. |
Changes to docs/manual/00001012052000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001012052000 title: API: Sort the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk If not specified, the list of zettel is sorted descending by the value of the zettel identifier. The highest zettel identifier, which is a number, comes first. You change that with the ""''_sort''"" query parameter. Alternatively, you can also use the ""''_order''"" query parameter. It is an alias. Its value is the name of a metadata key, optionally prefixed with a hyphen-minus character (""-"", ''U+002D''). According to the [[type|00001006030000]] of a metadata key, the list of zettel is sorted. If hyphen-minus is given, the order is descending, else ascending. If you want a random list of zettel, specify the value ""_random"" in place of the metadata key. ""''_sort=_random''"" (or ""''_order=_random''"") is the query parameter in this case. | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001012052000 title: API: Sort the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210709162242 If not specified, the list of zettel is sorted descending by the value of the zettel identifier. The highest zettel identifier, which is a number, comes first. You change that with the ""''_sort''"" query parameter. Alternatively, you can also use the ""''_order''"" query parameter. It is an alias. Its value is the name of a metadata key, optionally prefixed with a hyphen-minus character (""-"", ''U+002D''). According to the [[type|00001006030000]] of a metadata key, the list of zettel is sorted. If hyphen-minus is given, the order is descending, else ascending. If you want a random list of zettel, specify the value ""_random"" in place of the metadata key. ""''_sort=_random''"" (or ""''_order=_random''"") is the query parameter in this case. If can be combined with ""[[''_limit=1''|00001012051830]]"" to obtain just one random zettel. Currently, only the first occurrence of ''_sort'' is recognized. In the future it will be possible to specify a combined sort key. |
Added docs/manual/00001012053200.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163927 A zettel is created by adding it to the [[list of zettel|00001012000000#zettel-lists]]. Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/z'', but you must send the data of the new zettel via a HTTP POST request. The body of the POST request must contain a JSON object that specifies metadata and content of the zettel to be created. The following keys of the JSON object are used: ; ''"meta"'' : References an embedded JSON object with only string values. The name/value pairs of this objects are interpreted as the metadata of the new zettel. Please consider the [[list of supported metadata keys|00001006020000]] (and their value types). ; ''"encoding"'' : States how the content is encoded. Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]]. Other values will result in a HTTP response status code ''400''. ; ''"content"'' : Is a string value that contains the content of the zettel to be created. Typically, text content is not encoded, and binary content is encoded via Base64. Other keys will be ignored. Even these three keys are just optional. The body of the HTTP POST request must not be empty and it must contain a JSON object. Therefore, a body containing just ''{}'' is perfectly valid. The new zettel will have no content, its title will be set to the value of [[''default-title''|00001004020000#default-title]] (default: ""Untitled""), its role is set to the value of [[''default-role''|00001004020000#default-role]] (default: ""zettel""), and its syntax is set to the value of [[''default-syntax''|00001004020000#default-syntax]] (default: ""zmk""). ``` # curl -X POST --data '{}' http://127.0.0.1:23123/z {"id":"20210713161000","url":"/z/20210713161000"} ``` If creating the zettel was successful, the HTTP response will contain a JSON object with two keys: ; ''"id"'' : Contains the zettel identifier of the created zettel for further usage. ; ''"url"'' : The URL for [[reading metadata and content|00001012053400]] of the new zettel. In most cases, the URL is a relative one. A client must prepend the HTTP protocol scheme, the host name, and (optional, but often needed) the post number to make it an absolute URL. In addition, the HTTP response header contains a key ''Location'' with the same value of the relative URL. As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing: ``` # curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' http://127.0.0.1:23123/z {"id":"20210713163100","url":"/z/20210713163100"} ``` === HTTP Status codes ; ''201'' : Zettel creation was successful, the body contains a JSON object that contains its zettel identifier. ; ''400'' : Request was not valid. There are several reasons for this. Most likely, the JSON was not formed according to above rules. ; ''403'' : You are not allowed to create a new zettel. |
Changes to docs/manual/00001012053600.zettel.
1 2 3 4 | id: 00001012053600 title: API: Retrieve references of an existing zettel tags: #api #manual #zettelstore syntax: zmk | > | | < < < | < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | id: 00001012053600 title: API: Retrieve references of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210702183357 The web of zettel is one important value of a Zettelstore. Many zettel references other zettel, images, external/local material or, via citations, external literature. By using the [[endpoint|00001012920000]] ''/l/{ID}'' you are able to retrieve these references. ```` # curl http://127.0.0.1:23123/l/00001012053600 {"id":"00001012053600","url":"/z/00001012053600","links":{"outgoing":[{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001007040300","url":"/z/00001007040300#links"},{"id":"00001007040300","url":"/z/00001007040300#images"},{"id":"00001007040300","url":"/z/00001007040300#citation-key"}]},"images":{}} ```` Formatted, this translates into: ````json { "id": "00001012053600", "url": "/z/00001012053600", "links": { "outgoing": [ { "id": "00001012920000", "url": "/z/00001012920000" }, { "id": "00001007040300", "url": "/z/00001007040300#links" }, { "id": "00001007040300", "url": "/z/00001007040300#images" }, { "id": "00001007040300", "url": "/z/00001007040300#citation-key" } ], }, "images": {}, } ```` === Kind The following to-level JSON keys are returned: ; ''id'' : The zettel identifier for which the references were requested. ; ''url'' |
︙ | ︙ | |||
80 81 82 83 84 85 86 87 | This is controlled by the value of the query parameter ''matter'': |= ''matter''| Number >| Returned reference list | (nothing) | (30) | incoming, outgoing, local, and external references | ''incoming'' | 2|incoming reference, not allowed for images (aka ""backlinks"", not yet implemented) | ''outgoing'' | 4|outgoing references | ''local'' | 8|local references, i.e. local, non-zettel material | ''external'' |16| external references, i.e. on the web | ''zettel'' | (6)|incoming and outgoing references | > | | | 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | This is controlled by the value of the query parameter ''matter'': |= ''matter''| Number >| Returned reference list | (nothing) | (30) | incoming, outgoing, local, and external references | ''incoming'' | 2|incoming reference, not allowed for images (aka ""backlinks"", not yet implemented) | ''outgoing'' | 4|outgoing references | ''local'' | 8|local references, i.e. local, non-zettel material | ''external'' |16| external references, i.e. on the web | ''meta'' |32| external reference, stored in metadata | ''zettel'' | (6)|incoming and outgoing references | ''material'' |(56)| local and external references | ''all'' | (62)| incoming, outgoing, local, and external references Incoming and outgoing references are basically zettel. Therefore the list elements are JSON objects with keys ''id'' and ''url''. Local and external references are strings. Similar to the ''kind'' query parameter, each matter is associated with a number. To retrieve a combination of matter values that does not have a name, just add the numbers. |
︙ | ︙ |
Changes to docs/manual/00001012053800.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel. Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]]. Zettel are also connected by using same [[tags|00001006020000#tags]]. | > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210712223623 The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel. Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]]. Zettel are also connected by using same [[tags|00001006020000#tags]]. The context is defined by a //direction//, a //depth//, and a //limit//: * Direction: connections are directed. For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''formward'' list all zettel to which the current zettel links. When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"". All other values, including a missing value, is interpreted as ""both"". * Depth: a direct connection has depth 1, an indirect connection is the length of the shortest path between two zettel. You should limit the depth by using the parameter ''depth''. Its default value is ""5"". A value of ""0"" does disable any depth check. * Limit: to set an upper bound for the returned context, you should use the parameter ''limit''. Its default value is ""200"". A value of ""0"" disables does not limit the number of elements returned. Zettel with same tags as the origin zettel are considered depth 1. Only for the origin zettel, tags are used to calculate a connection. Currently, only some of the newest zettel with a given tag are considered a connection.[^The number of zettel is given by the value of parameter ''depth''.] Otherwise the context would become too big and therefore unusable. To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/x/{ID}''[^Mnemonic: conte**X**t]. ```` # curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2' {"id": "00001012053800","url": "/z/00001012053800","meta": {...},"list": [{"id": "00001012921000","url": "/z/00001012921000","meta": {...}},{"id": "00001012920800","url": "/z/00001012920800","meta": {...}},{"id": "00010000000000","url": "/z/00010000000000","meta": {...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001012053800", "url": "/z/00001012053800", |
︙ | ︙ |
Changes to docs/manual/00001012054000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012054000 title: API: Retrieve zettel order within an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk Some zettel act as a ""table of contents"" for other zettel. The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. Every zettel with a certain internal structure can act as the ""table of contents"" for others. What is a ""table of contents""? Basically, it is just a list of references to other zettel. | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012054000 title: API: Retrieve zettel order within an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721184434 Some zettel act as a ""table of contents"" for other zettel. The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. Every zettel with a certain internal structure can act as the ""table of contents"" for others. What is a ""table of contents""? Basically, it is just a list of references to other zettel. |
︙ | ︙ | |||
25 26 27 28 29 30 31 | {"id":"00001000000000","url":"/z/00001000000000","meta":{...},"list":[{"id":"00001001000000","url":"/z/00001001000000","meta":{...}},{"id":"00001002000000","url":"/z/00001002000000","meta":{...}},{"id":"00001003000000","url":"/z/00001003000000","meta":{...}},{"id":"00001004000000","url":"/z/00001004000000","meta":{...}},...,{"id":"00001014000000","url":"/z/00001014000000","meta":{...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001000000000", "url": "/z/00001000000000", | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | {"id":"00001000000000","url":"/z/00001000000000","meta":{...},"list":[{"id":"00001001000000","url":"/z/00001001000000","meta":{...}},{"id":"00001002000000","url":"/z/00001002000000","meta":{...}},{"id":"00001003000000","url":"/z/00001003000000","meta":{...}},{"id":"00001004000000","url":"/z/00001004000000","meta":{...}},...,{"id":"00001014000000","url":"/z/00001014000000","meta":{...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001000000000", "url": "/z/00001000000000", "list": [ { "id": "00001001000000", "url": "/z/00001001000000", "meta": {...} }, { "id": "00001002000000", |
︙ | ︙ |
Added docs/manual/00001012054200.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163606 Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]]. In both cases you must provide the data for the new or updated zettel in the body of the HTTP request. One difference is the endpoint. The [[endpoint|00001012920000]] to update a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). You must send a HTTP PUT request to that endpoint: ``` # curl -X PUT --data '{}' http://127.0.0.1:23123/z/00001012054200 ``` This will put some empty content and metadata to the zettel you are currently reading. As usual, some metadata will be calculated if it is empty. The body of the HTTP response is empty, if the request was successful. === HTTP Status codes ; ''204'' : Update was successful, there is no body in the response. ; ''400'' : Request was not valid. For example, the request body was not valid. ; ''403'' : You are not allowed to delete the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Added docs/manual/00001012054400.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163708 Renaming a zettel is effectively just specifying a new identifier for the zettel. Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful. If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations. As a consequence, you cannot rename a zettel when its identifier is used in a read-only box. This applies to all predefined zettel, for example. The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''. ``` # curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/z/00001000000000 ``` Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused zettel identifier. If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''. All other characters, besides those 14 digits, are effectively ignored. However, the value should form a valid URL that could be used later to [[read the content|00001012053400]] of the freshly renamed zettel. === HTTP Status codes ; ''204'' : Rename was successful, there is no body in the response. ; ''400'' : Request was not valid. For example, the HTTP header did not contain a valid ''Destination'' key, or the new identifier is already in use. ; ''403'' : You are not allowed to delete the given zettel. In most cases you have either not enough [[access rights|00001010070600]] or at least one box containing the given identifier operates in read-only mode. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. === Rationale for the MOVE method HTTP [[standardizes|https://www.rfc-editor.org/rfc/rfc7231.txt]] seven methods. None of them is conceptually close to a rename operation. Everyone is free to ""invent"" some new method to be used in HTTP. To avoid a divergency, there is a [[methods registry|https://www.iana.org/assignments/http-methods/]] that tracks those extensions. The [[HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)|https://www.rfc-editor.org/rfc/rfc4918.txt]] defines the method MOVE that is quite close to the desired rename operation. In fact, some command line tools use a ""move"" method for renaming files. Therefore, Zettelstore adopts somehow WebDAV's MOVE method and its use of the ''Destination'' HTTP header key. |
Added docs/manual/00001012054600.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001012054600 title: API: Delete a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163722 Deleting a zettel within the Zettelstore is executed on the first [[box|00001004011200]] that contains that zettel. Zettel with the same identifier, but in subsequent boxes remain. If the first box containing the zettel is read-only, deleting that zettel will fail, as well for a Zettelstore in [[read-only mode|00001004010000#read-only-mode]] or if authentication is enabled and the user has no [[access right|00001010070600]] to do so. The [[endpoint|00001012920000]] to delete a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). You must send a HTTP DELETE request to this endpoint: ``` # curl -X DELETE http://127.0.0.1:23123/z/00001000000000 ``` === HTTP Status codes ; ''204'' : Delete was successful, there is no body in the response. ; ''403'' : You are not allowed to delete the given zettel. Maybe you do not have enough access rights, or either the box or Zettelstore itself operate in read-only mode. ; ''404'' : Zettel not found. You probably specified a zettel identifier that is not used in the Zettelstore. |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk | | | < < | | | | > > | | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20210712225257 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ''/''), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' : is a single letter that specifies the ressource type, ; ''ZETTEL-ID'' : is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]]. The following letters are currently in use: |= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic | ''l'' | | GET: [[list references|00001012053600]] | **L**inks | ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder | ''r'' | GET: [[list roles|00001012052400]] | | **R**oles | ''t'' | GET: [[list tags|00001012052200]] || **T**ags | ''v'' | POST: [[client authentication|00001012050200]] | | **V**erbürgen[^German translation for ""authentication"", since ''/a'' is already in use by the [[web user interface|00001014000000]].] | | PUT: [[renew access token|00001012050400]] | | ''x'' | | GET: [[list zettel context|00001012053800]] | Conte**x**t | ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053400]] | **Z**ettel | | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] | | | DELETE: [[delete the zettel|00001012054600]] | | | MOVE: [[rename the zettel|00001012054400]] The full URL will contain either the ''http'' oder ''https'' scheme, a host name, and an optional port number. The API examples will assume the ''http'' schema, the local host ''127.0.0.1'', the default port ''23123'', and the default empty ''PREFIX''. Therefore, all URLs in the API documentation will begin with ''http://127.0.0.1:23123''. |
Changes to domain/content.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain | > > > | > > > | | > > | > | > | > > > > > > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import ( "encoding/base64" "errors" "unicode/utf8" "zettelstore.de/z/strfun" ) // Content is just the content of a zettel. type Content struct { data string isBinary bool } // NewContent creates a new content from a string. func NewContent(s string) Content { return Content{data: s, isBinary: calcIsBinary(s)} } // Set content to new string value. func (zc *Content) Set(s string) { zc.data = s zc.isBinary = calcIsBinary(s) } // AsString returns the content itself is a string. func (zc *Content) AsString() string { return zc.data } // AsBytes returns the content itself is a byte slice. func (zc *Content) AsBytes() []byte { return []byte(zc.data) } // IsBinary returns true if the content contains non-unicode values or is, // interpreted a text, with a high probability binary content. func (zc *Content) IsBinary() bool { return zc.isBinary } // TrimSpace remove some space character in content, if it is not binary content. func (zc *Content) TrimSpace() { if zc.isBinary { return } zc.data = strfun.TrimSpaceRight(zc.data) } // Encode content for future transmission. func (zc *Content) Encode() (data, encoding string) { if !zc.isBinary { return zc.data, "" } return base64.StdEncoding.EncodeToString([]byte(zc.data)), "base64" } // SetDecoded content to the decoded value of the given string. func (zc *Content) SetDecoded(data, encoding string) error { switch encoding { case "": zc.data = data case "base64": decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } zc.data = string(decoded) default: return errors.New("unknown encoding " + encoding) } zc.isBinary = calcIsBinary(zc.data) return nil } func calcIsBinary(s string) bool { if !utf8.ValidString(s) { return true } l := len(s) for i := 0; i < l; i++ { if s[i] == 0 { return true } } return false } |
Changes to domain/content_test.go.
1 | //----------------------------------------------------------------------------- | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package domain_test import ( "testing" "zettelstore.de/z/domain" ) func TestContentIsBinary(t *testing.T) { t.Parallel() td := []struct { s string exp bool }{ {"abc", false}, {"äöü", false}, {"", false}, |
︙ | ︙ |
Changes to domain/id/id.go.
︙ | ︙ | |||
21 22 23 24 25 26 27 | // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 // Some important ZettelIDs. // Note: if you change some values, ensure that you also change them in the | | | > | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 // Some important ZettelIDs. // Note: if you change some values, ensure that you also change them in the // constant box. They are mentioned there literally, because these // constants are not available there. const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid // System zettel VersionZid = Zid(1) HostZid = Zid(2) OperatingSystemZid = Zid(3) LicenseZid = Zid(4) AuthorsZid = Zid(5) DependenciesZid = Zid(6) BoxManagerZid = Zid(20) MetadataKeyZid = Zid(90) StartupConfigurationZid = Zid(96) ConfigurationZid = Zid(100) // WebUI HTML templates are in the range 10000..19999 BaseTemplateZid = Zid(10100) LoginTemplateZid = Zid(10200) ListTemplateZid = Zid(10300) ZettelTemplateZid = Zid(10401) InfoTemplateZid = Zid(10402) FormTemplateZid = Zid(10403) RenameTemplateZid = Zid(10404) DeleteTemplateZid = Zid(10405) ContextTemplateZid = Zid(10406) RolesTemplateZid = Zid(10500) TagsTemplateZid = Zid(10600) ErrorTemplateZid = Zid(10700) // WebUI CSS zettel are in the range 20000..29999 BaseCSSZid = Zid(20001) UserCSSZid = Zid(25001) // WebUI JS zettel are in the range 30000..39999 // WebUI image zettel are in the range 40000..49999 EmojiZid = Zid(40001) // Range 90000...99999 is reserved for zettel templates |
︙ | ︙ |
Changes to domain/id/id_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestIsValid(t *testing.T) { validIDs := []string{ "00000000000001", "00000000000020", "00000000000300", "00000000004000", "00000000050000", "00000000600000", | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestIsValid(t *testing.T) { t.Parallel() validIDs := []string{ "00000000000001", "00000000000020", "00000000000300", "00000000004000", "00000000050000", "00000000600000", |
︙ | ︙ |
Changes to domain/id/set_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSetSorted(t *testing.T) { testcases := []struct { set id.Set exp id.Slice }{ {nil, nil}, {id.NewSet(), nil}, {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, } for i, tc := range testcases { got := tc.set.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) } } } func TestSetIntersection(t *testing.T) { testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, | > > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSetSorted(t *testing.T) { t.Parallel() testcases := []struct { set id.Set exp id.Slice }{ {nil, nil}, {id.NewSet(), nil}, {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, } for i, tc := range testcases { got := tc.set.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) } } } func TestSetIntersection(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, |
︙ | ︙ | |||
59 60 61 62 63 64 65 66 67 68 69 70 71 72 | if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got) } } } func TestSetRemove(t *testing.T) { testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, | > | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got) } } } func TestSetRemove(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, |
︙ | ︙ |
Changes to domain/id/slice_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSliceSort(t *testing.T) { zs := id.Slice{9, 4, 6, 1, 7} zs.Sort() exp := id.Slice{1, 4, 6, 7, 9} if !zs.Equal(exp) { t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) } } func TestCopy(t *testing.T) { var orig id.Slice got := orig.Copy() if got != nil { t.Errorf("Nil copy resulted in %v", got) } orig = id.Slice{9, 4, 6, 1, 7} got = orig.Copy() if !orig.Equal(got) { t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) } } func TestSliceEqual(t *testing.T) { testcases := []struct { s1, s2 id.Slice exp bool }{ {nil, nil, true}, {nil, id.Slice{}, true}, {nil, id.Slice{1}, false}, | > > > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSliceSort(t *testing.T) { t.Parallel() zs := id.Slice{9, 4, 6, 1, 7} zs.Sort() exp := id.Slice{1, 4, 6, 7, 9} if !zs.Equal(exp) { t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) } } func TestCopy(t *testing.T) { t.Parallel() var orig id.Slice got := orig.Copy() if got != nil { t.Errorf("Nil copy resulted in %v", got) } orig = id.Slice{9, 4, 6, 1, 7} got = orig.Copy() if !orig.Equal(got) { t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) } } func TestSliceEqual(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Slice exp bool }{ {nil, nil, true}, {nil, id.Slice{}, true}, {nil, id.Slice{1}, false}, |
︙ | ︙ | |||
62 63 64 65 66 67 68 69 70 71 72 73 74 75 | if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) } } } func TestSliceString(t *testing.T) { testcases := []struct { in id.Slice exp string }{ {nil, ""}, {id.Slice{}, ""}, {id.Slice{1}, "00000000000001"}, | > | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) } } } func TestSliceString(t *testing.T) { t.Parallel() testcases := []struct { in id.Slice exp string }{ {nil, ""}, {id.Slice{}, ""}, {id.Slice{1}, "00000000000001"}, |
︙ | ︙ |
Changes to domain/meta/meta.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "regexp" "sort" "strings" "zettelstore.de/z/domain/id" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "regexp" "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) type keyUsage int const ( _ keyUsage = iota usageUser // Key will be manipulated by the user |
︙ | ︙ | |||
85 86 87 88 89 90 91 | } // GetDescription returns the key description object of the given key name. func GetDescription(name string) DescriptionKey { if d, ok := registeredKeys[name]; ok { return *d } | | | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | } // GetDescription returns the key description object of the given key name. func GetDescription(name string) DescriptionKey { if d, ok := registeredKeys[name]; ok { return *d } return DescriptionKey{Type: Type(name)} } // GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. func GetSortedKeyDescriptions() []*DescriptionKey { names := make([]string, 0, len(registeredKeys)) for n := range registeredKeys { names = append(names, n) |
︙ | ︙ | |||
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | KeyID = registerKey("id", TypeID, usageComputed, "") KeyTitle = registerKey("title", TypeZettelmarkup, usageUser, "") KeyRole = registerKey("role", TypeWord, usageUser, "") KeyTags = registerKey("tags", TypeTagSet, usageUser, "") KeySyntax = registerKey("syntax", TypeWord, usageUser, "") KeyBack = registerKey("back", TypeIDSet, usageProperty, "") KeyBackward = registerKey("backward", TypeIDSet, usageProperty, "") KeyCopyright = registerKey("copyright", TypeString, usageUser, "") KeyCredential = registerKey("credential", TypeCredential, usageUser, "") KeyDead = registerKey("dead", TypeIDSet, usageProperty, "") KeyDefaultCopyright = registerKey("default-copyright", TypeString, usageUser, "") KeyDefaultLang = registerKey("default-lang", TypeWord, usageUser, "") KeyDefaultLicense = registerKey("default-license", TypeEmpty, usageUser, "") KeyDefaultRole = registerKey("default-role", TypeWord, usageUser, "") KeyDefaultSyntax = registerKey("default-syntax", TypeWord, usageUser, "") KeyDefaultTitle = registerKey("default-title", TypeZettelmarkup, usageUser, "") KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "") KeyDuplicates = registerKey("duplicates", TypeBool, usageUser, "") KeyExpertMode = registerKey("expert-mode", TypeBool, usageUser, "") KeyFolge = registerKey("folge", TypeIDSet, usageProperty, "") KeyFooterHTML = registerKey("footer-html", TypeString, usageUser, "") KeyForward = registerKey("forward", TypeIDSet, usageProperty, "") KeyHomeZettel = registerKey("home-zettel", TypeID, usageUser, "") KeyLang = registerKey("lang", TypeWord, usageUser, "") KeyLicense = registerKey("license", TypeEmpty, usageUser, "") | > < > > > > > | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | KeyID = registerKey("id", TypeID, usageComputed, "") KeyTitle = registerKey("title", TypeZettelmarkup, usageUser, "") KeyRole = registerKey("role", TypeWord, usageUser, "") KeyTags = registerKey("tags", TypeTagSet, usageUser, "") KeySyntax = registerKey("syntax", TypeWord, usageUser, "") KeyBack = registerKey("back", TypeIDSet, usageProperty, "") KeyBackward = registerKey("backward", TypeIDSet, usageProperty, "") KeyBoxNumber = registerKey("box-number", TypeNumber, usageComputed, "") KeyCopyright = registerKey("copyright", TypeString, usageUser, "") KeyCredential = registerKey("credential", TypeCredential, usageUser, "") KeyDead = registerKey("dead", TypeIDSet, usageProperty, "") KeyDefaultCopyright = registerKey("default-copyright", TypeString, usageUser, "") KeyDefaultLang = registerKey("default-lang", TypeWord, usageUser, "") KeyDefaultLicense = registerKey("default-license", TypeEmpty, usageUser, "") KeyDefaultRole = registerKey("default-role", TypeWord, usageUser, "") KeyDefaultSyntax = registerKey("default-syntax", TypeWord, usageUser, "") KeyDefaultTitle = registerKey("default-title", TypeZettelmarkup, usageUser, "") KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "") KeyDuplicates = registerKey("duplicates", TypeBool, usageUser, "") KeyExpertMode = registerKey("expert-mode", TypeBool, usageUser, "") KeyFolge = registerKey("folge", TypeIDSet, usageProperty, "") KeyFooterHTML = registerKey("footer-html", TypeString, usageUser, "") KeyForward = registerKey("forward", TypeIDSet, usageProperty, "") KeyHomeZettel = registerKey("home-zettel", TypeID, usageUser, "") KeyLang = registerKey("lang", TypeWord, usageUser, "") KeyLicense = registerKey("license", TypeEmpty, usageUser, "") KeyMarkerExternal = registerKey("marker-external", TypeEmpty, usageUser, "") KeyModified = registerKey("modified", TypeTimestamp, usageComputed, "") KeyNoIndex = registerKey("no-index", TypeBool, usageUser, "") KeyPrecursor = registerKey("precursor", TypeIDSet, usageUser, KeyFolge) KeyPublished = registerKey("published", TypeTimestamp, usageProperty, "") KeyReadOnly = registerKey("read-only", TypeWord, usageUser, "") KeySiteName = registerKey("site-name", TypeString, usageUser, "") KeyURL = registerKey("url", TypeURL, usageUser, "") KeyUserID = registerKey("user-id", TypeWord, usageUser, "") KeyUserRole = registerKey("user-role", TypeWord, usageUser, "") KeyVisibility = registerKey("visibility", TypeWord, usageUser, "") KeyYAMLHeader = registerKey("yaml-header", TypeBool, usageUser, "") KeyZettelFileSyntax = registerKey("zettel-file-syntax", TypeWordSet, usageUser, "") ) // NewPrefix is the prefix for metadata key in template zettel for creating new zettel. const NewPrefix = "new-" // Important values for some keys. const ( ValueRoleConfiguration = "configuration" ValueRoleUser = "user" ValueRoleZettel = "zettel" ValueSyntaxNone = "none" ValueSyntaxGif = "gif" ValueSyntaxText = "text" ValueSyntaxZmk = "zmk" ValueTrue = "true" ValueFalse = "false" ValueLangEN = "en" ValueUserRoleCreator = "creator" ValueUserRoleReader = "reader" ValueUserRoleWriter = "writer" ValueUserRoleOwner = "owner" ValueVisibilityCreator = "creator" ValueVisibilityExpert = "expert" ValueVisibilityOwner = "owner" ValueVisibilityLogin = "login" ValueVisibilityPublic = "public" ) // Meta contains all meta-data of a zettel. |
︙ | ︙ | |||
227 228 229 230 231 232 233 | func (m *Meta) Set(key, value string) { if key != KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { | | | 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | func (m *Meta) Set(key, value string) { if key != KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { return strings.TrimFunc(value, input.IsSpace) } // Get retrieves the string value of a given key. The bool value signals, // whether there was a value stored or not. func (m *Meta) Get(key string) (string, bool) { if key == KeyID { return m.Zid.String(), true |
︙ | ︙ |
Changes to domain/meta/meta_test.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) func TestKeyIsValid(t *testing.T) { validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} for _, key := range validKeys { if !KeyIsValid(key) { t.Errorf("Key %q wrongly identified as invalid key", key) } } invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} for _, key := range invalidKeys { if KeyIsValid(key) { t.Errorf("Key %q wrongly identified as valid key", key) } } } func TestTitleHeader(t *testing.T) { m := New(testID) if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } addToMeta(m, KeyTitle, " ") if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) func TestKeyIsValid(t *testing.T) { t.Parallel() validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} for _, key := range validKeys { if !KeyIsValid(key) { t.Errorf("Key %q wrongly identified as invalid key", key) } } invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} for _, key := range invalidKeys { if KeyIsValid(key) { t.Errorf("Key %q wrongly identified as valid key", key) } } } func TestTitleHeader(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } addToMeta(m, KeyTitle, " ") if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) |
︙ | ︙ | |||
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | } if len(exp) < len(got) { t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { m := New(testID) checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, "") checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, " #t1 #t2 #t3 #t4 ") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags) addToMeta(m, KeyTags, "#t5") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) addToMeta(m, KeyTags, "t6") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) } func TestSyntax(t *testing.T) { m := New(testID) if got, ok := m.Get(KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, " ") if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) | > > | 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | } if len(exp) < len(got) { t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, "") checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, " #t1 #t2 #t3 #t4 ") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags) addToMeta(m, KeyTags, "#t5") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) addToMeta(m, KeyTags, "t6") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) } func TestSyntax(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, " ") if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) |
︙ | ︙ | |||
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | t.Errorf("Key %q missing, should have value %q", k, v) } } } } func TestDefaultHeader(t *testing.T) { m := New(testID) addToMeta(m, "h1", "d1") addToMeta(m, "H2", "D2") addToMeta(m, "H1", "D1.1") exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "", "d0") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", "") exp["h3"] = "" checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", " ") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h4", " ") exp["h4"] = "" checkHeader(t, exp, m.Pairs(true)) } func TestDelete(t *testing.T) { m := New(testID) m.Set("key", "val") if got, ok := m.Get("key"); !ok || got != "val" { t.Errorf("Value != %q, got: %v/%q", "val", ok, got) } m.Set("key", "") if got, ok := m.Get("key"); !ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } m.Delete("key") if got, ok := m.Get("key"); ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } } func TestEqual(t *testing.T) { testcases := []struct { pairs1, pairs2 []string allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, | > > > | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | t.Errorf("Key %q missing, should have value %q", k, v) } } } } func TestDefaultHeader(t *testing.T) { t.Parallel() m := New(testID) addToMeta(m, "h1", "d1") addToMeta(m, "H2", "D2") addToMeta(m, "H1", "D1.1") exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "", "d0") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", "") exp["h3"] = "" checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", " ") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h4", " ") exp["h4"] = "" checkHeader(t, exp, m.Pairs(true)) } func TestDelete(t *testing.T) { t.Parallel() m := New(testID) m.Set("key", "val") if got, ok := m.Get("key"); !ok || got != "val" { t.Errorf("Value != %q, got: %v/%q", "val", ok, got) } m.Set("key", "") if got, ok := m.Get("key"); !ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } m.Delete("key") if got, ok := m.Get("key"); ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } } func TestEqual(t *testing.T) { t.Parallel() testcases := []struct { pairs1, pairs2 []string allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, |
︙ | ︙ |
Changes to domain/meta/parse.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) // NewFromInput parses the meta data of a zettel. func NewFromInput(zid id.Zid, inp *input.Input) *Meta { if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { skipToEOL(inp) inp.EatEOL() |
︙ | ︙ | |||
68 69 70 71 72 73 74 | var val string for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val += inp.Src[pos:inp.Pos] inp.EatEOL() | | | | 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | var val string for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val += inp.Src[pos:inp.Pos] inp.EatEOL() if !input.IsSpace(inp.Ch) { break } val += " " } addToMeta(m, key, val) } func skipSpace(inp *input.Input) { for input.IsSpace(inp.Ch) { inp.Next() } } func skipToEOL(inp *input.Input) { for { switch inp.Ch { |
︙ | ︙ | |||
120 121 122 123 124 125 126 | if !ok { oldElems = nil } set := make(map[string]bool, len(newElems)+len(oldElems)) addToSet(set, newElems, useElem) if len(set) == 0 { | | | 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | if !ok { oldElems = nil } set := make(map[string]bool, len(newElems)+len(oldElems)) addToSet(set, newElems, useElem) if len(set) == 0 { // Nothing to add. Maybe because of rejected elements. return } addToSet(set, oldElems, useElem) resultList := make([]string, 0, len(set)) for tag := range set { resultList = append(resultList, tag) |
︙ | ︙ |
Changes to domain/meta/parse_test.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput(src)) } func TestEmpty(t *testing.T) { m := parseMetaStr("") if got, ok := m.Get(meta.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { td := []struct{ s, e string }{ {meta.KeyTitle + ": a title", "a title"}, {meta.KeyTitle + ": a\n\t title", "a title"}, {meta.KeyTitle + ": a\n\t title\r\n x", "a title x"}, {meta.KeyTitle + " AbC", "AbC"}, {meta.KeyTitle + " AbC\n ded", "AbC ded"}, {meta.KeyTitle + ": o\ntitle: p", "o p"}, | > > | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput(src)) } func TestEmpty(t *testing.T) { t.Parallel() m := parseMetaStr("") if got, ok := m.Get(meta.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { t.Parallel() td := []struct{ s, e string }{ {meta.KeyTitle + ": a title", "a title"}, {meta.KeyTitle + ": a\n\t title", "a title"}, {meta.KeyTitle + ": a\n\t title\r\n x", "a title x"}, {meta.KeyTitle + " AbC", "AbC"}, {meta.KeyTitle + " AbC\n ded", "AbC ded"}, {meta.KeyTitle + ": o\ntitle: p", "o p"}, |
︙ | ︙ | |||
57 58 59 60 61 62 63 64 65 66 67 68 69 70 | m := parseMetaStr(meta.KeyTitle + ": ") if title, ok := m.Get(meta.KeyTitle); ok { t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m) } } func TestNewFromInput(t *testing.T) { testcases := []struct { input string exp []meta.Pair }{ {"", []meta.Pair{}}, {" a:b", []meta.Pair{{"a", "b"}}}, {"%a:b", []meta.Pair{}}, | > | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | m := parseMetaStr(meta.KeyTitle + ": ") if title, ok := m.Get(meta.KeyTitle); ok { t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m) } } func TestNewFromInput(t *testing.T) { t.Parallel() testcases := []struct { input string exp []meta.Pair }{ {"", []meta.Pair{}}, {" a:b", []meta.Pair{{"a", "b"}}}, {"%a:b", []meta.Pair{}}, |
︙ | ︙ | |||
105 106 107 108 109 110 111 112 113 114 115 116 117 118 | return false } } return true } func TestPrecursorIDSet(t *testing.T) { var testdata = []struct { inp string exp string }{ {"", ""}, {"123", ""}, {"12345678901234", "12345678901234"}, | > | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | return false } } return true } func TestPrecursorIDSet(t *testing.T) { t.Parallel() var testdata = []struct { inp string exp string }{ {"", ""}, {"123", ""}, {"12345678901234", "12345678901234"}, |
︙ | ︙ |
Changes to domain/meta/type.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package meta provides the domain specific type 'meta'. package meta import ( "strconv" "strings" "time" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool | > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package meta provides the domain specific type 'meta'. package meta import ( "strconv" "strings" "sync" "time" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool |
︙ | ︙ | |||
45 46 47 48 49 50 51 | TypeID = registerType("Identifier", false) TypeIDSet = registerType("IdentifierSet", true) TypeNumber = registerType("Number", false) TypeString = registerType("String", false) TypeTagSet = registerType("TagSet", true) TypeTimestamp = registerType("Timestamp", false) TypeURL = registerType("URL", false) | < > > > > > > > > > > > > > > > > > > > > > > > > > > > | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | TypeID = registerType("Identifier", false) TypeIDSet = registerType("IdentifierSet", true) TypeNumber = registerType("Number", false) TypeString = registerType("String", false) TypeTagSet = registerType("TagSet", true) TypeTimestamp = registerType("Timestamp", false) TypeURL = registerType("URL", false) TypeWord = registerType("Word", false) TypeWordSet = registerType("WordSet", true) TypeZettelmarkup = registerType("Zettelmarkup", false) ) // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func (m *Meta) Type(key string) *DescriptionType { return Type(key) } var ( cachedTypedKeys = make(map[string]*DescriptionType) mxTypedKey sync.RWMutex ) func typedKey(key string, t *DescriptionType) *DescriptionType { mxTypedKey.Lock() defer mxTypedKey.Unlock() cachedTypedKeys[key] = t return t } // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func Type(key string) *DescriptionType { if k, ok := registeredKeys[key]; ok { return k.Type } mxTypedKey.RLock() k, ok := cachedTypedKeys[key] mxTypedKey.RUnlock() if ok { return k } if strings.HasSuffix(key, "-url") { return typedKey(key, TypeURL) } if strings.HasSuffix(key, "-number") { return typedKey(key, TypeNumber) } if strings.HasSuffix(key, "-zid") { return typedKey(key, TypeID) } return TypeEmpty } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { if key != KeyID { for i, val := range values { values[i] = trimValue(val) |
︙ | ︙ |
Changes to domain/meta/type_test.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestNow(t *testing.T) { m := meta.New(id.Invalid) m.SetNow("key") val, ok := m.Get("key") if !ok { t.Error("Unable to get value of key") } if len(val) != 14 { t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } if _, ok := m.GetTime("key"); !ok { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { testCases := []struct { value string valid bool exp time.Time }{ {"", false, time.Time{}}, {"1", false, time.Time{}}, | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestNow(t *testing.T) { t.Parallel() m := meta.New(id.Invalid) m.SetNow("key") val, ok := m.Get("key") if !ok { t.Error("Unable to get value of key") } if len(val) != 14 { t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } if _, ok := m.GetTime("key"); !ok { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { t.Parallel() testCases := []struct { value string valid bool exp time.Time }{ {"", false, time.Time{}}, {"1", false, time.Time{}}, |
︙ | ︙ |
Changes to domain/meta/values.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 | type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ | > | > | | | > > | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityCreator VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ ValueVisibilityPublic: VisibilityPublic, ValueVisibilityCreator: VisibilityCreator, ValueVisibilityLogin: VisibilityLogin, ValueVisibilityOwner: VisibilityOwner, ValueVisibilityExpert: VisibilityExpert, } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { return vis } return VisibilityUnknown } // UserRole enumerates the supported values of meta key 'user-role'. type UserRole int // Supported values for user roles. const ( _ UserRole = iota UserRoleUnknown UserRoleCreator UserRoleReader UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ ValueUserRoleCreator: UserRoleCreator, ValueUserRoleReader: UserRoleReader, ValueUserRoleWriter: UserRoleWriter, ValueUserRoleOwner: UserRoleOwner, } // GetUserRole role returns the user role of the given string. func GetUserRole(val string) UserRole { if ur, ok := urMap[val]; ok { return ur } return UserRoleUnknown } |
Changes to domain/meta/write_test.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | m.Write(&sb, true) if got := sb.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { assertWriteMeta(t, newMeta("", nil, ""), "") m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") m = newMeta("TITLE", nil, "") m.Set("user", "zettel") m.Set("auth", "basic") assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") } | > | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | m.Write(&sb, true) if got := sb.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { t.Parallel() assertWriteMeta(t, newMeta("", nil, ""), "") m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") m = newMeta("TITLE", nil, "") m.Set("user", "zettel") m.Set("auth", "basic") assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") } |
Changes to encoder/buffer.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
28 29 30 31 32 33 34 | // NewBufWriter creates a new BufWriter func NewBufWriter(w io.Writer) BufWriter { return BufWriter{w: w, buf: make([]byte, 0, 4096)} } // Write writes the contents of p into the buffer. func (w *BufWriter) Write(p []byte) (int, error) { | | > > | | | | | | | | | < | < | > | > > > > > | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // NewBufWriter creates a new BufWriter func NewBufWriter(w io.Writer) BufWriter { return BufWriter{w: w, buf: make([]byte, 0, 4096)} } // Write writes the contents of p into the buffer. func (w *BufWriter) Write(p []byte) (int, error) { if w.err != nil { return 0, w.err } w.buf = append(w.buf, p...) if len(w.buf) > 2048 { w.flush() if w.err != nil { return 0, w.err } } return len(p), nil } // WriteString writes the contents of s into the buffer. func (w *BufWriter) WriteString(s string) { if w.err != nil { return } w.buf = append(w.buf, s...) if len(w.buf) > 2048 { w.flush() } } // WriteStrings writes the contents of sl into the buffer. func (w *BufWriter) WriteStrings(sl ...string) { for _, s := range sl { w.WriteString(s) } |
︙ | ︙ |
Changes to encoder/encfun/encfun.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Package encfun provides some helper function to work with encodings. package encfun import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) // MetaAsInlineSlice returns the value of the given metadata key as an inlince slice. func MetaAsInlineSlice(m *meta.Meta, key string) ast.InlineSlice { return parser.ParseMetadata(m.GetDefault(key, "")) } // MetaAsText returns the value of given metadata as text. func MetaAsText(m *meta.Meta, key string) string { | > | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // Package encfun provides some helper function to work with encodings. package encfun import ( "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) // MetaAsInlineSlice returns the value of the given metadata key as an inlince slice. func MetaAsInlineSlice(m *meta.Meta, key string) ast.InlineSlice { return parser.ParseMetadata(m.GetDefault(key, "")) } // MetaAsText returns the value of given metadata as text. func MetaAsText(m *meta.Meta, key string) string { textEncoder := encoder.Create(api.EncoderText, nil) var sb strings.Builder _, err := textEncoder.WriteInlines(&sb, MetaAsInlineSlice(m, key)) if err == nil { return sb.String() } return "" } |
Changes to encoder/encoder.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | // tree into some text form. package encoder import ( "errors" "io" "log" | < > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // tree into some text form. package encoder import ( "errors" "io" "log" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { WriteZettel(io.Writer, *ast.ZettelNode, bool) (int, error) |
︙ | ︙ | |||
37 38 39 40 41 42 43 | ErrNoWriteMeta = errors.New("method WriteMeta is not implemented") ErrNoWriteContent = errors.New("method WriteContent is not implemented") ErrNoWriteBlocks = errors.New("method WriteBlocks is not implemented") ErrNoWriteInlines = errors.New("method WriteInlines is not implemented") ) // Create builds a new encoder with the given options. | | | | | | | | | < | | | | | | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | ErrNoWriteMeta = errors.New("method WriteMeta is not implemented") ErrNoWriteContent = errors.New("method WriteContent is not implemented") ErrNoWriteBlocks = errors.New("method WriteBlocks is not implemented") ErrNoWriteInlines = errors.New("method WriteInlines is not implemented") ) // Create builds a new encoder with the given options. func Create(format api.EncodingEnum, env *Environment) Encoder { if info, ok := registry[format]; ok { return info.Create(env) } return nil } // Info stores some data about an encoder. type Info struct { Create func(*Environment) Encoder Default bool } var registry = map[api.EncodingEnum]Info{} var defFormat api.EncodingEnum // Register the encoder for later retrieval. func Register(format api.EncodingEnum, info Info) { if _, ok := registry[format]; ok { log.Fatalf("Writer with format %q already registered", format) } if info.Default { if defFormat != api.EncoderUnknown && defFormat != format { log.Fatalf("Default format already set: %q, new format: %q", defFormat, format) } defFormat = format } registry[format] = info } // GetFormats returns all registered formats, ordered by format code. func GetFormats() []api.EncodingEnum { result := make([]api.EncodingEnum, 0, len(registry)) for format := range registry { result = append(result, format) } return result } // GetDefaultFormat returns the format that should be used as default. func GetDefaultFormat() api.EncodingEnum { if defFormat != api.EncoderUnknown { return defFormat } if _, ok := registry[api.EncoderJSON]; ok { return api.EncoderJSON } log.Fatalf("No default format given") return api.EncoderUnknown } |
Changes to encoder/htmlenc/block.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "fmt" "strconv" "strings" "zettelstore.de/z/ast" ) | < < < < < < < < | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "fmt" "strconv" "strings" "zettelstore.de/z/ast" ) func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { switch vn.Kind { case ast.VerbatimProg: oldVisible := v.visibleSpace if vn.Attrs != nil { v.visibleSpace = vn.Attrs.HasDefault() } v.b.WriteString("<pre><code") v.visitAttributes(vn.Attrs) |
︙ | ︙ | |||
57 58 59 60 61 62 63 | case ast.VerbatimHTML: for _, line := range vn.Lines { if !ignoreHTMLText(line) { v.b.WriteStrings(line, "\n") } } default: | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | case ast.VerbatimHTML: for _, line := range vn.Lines { if !ignoreHTMLText(line) { v.b.WriteStrings(line, "\n") } } default: panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind)) } } var htmlSnippetsIgnore = []string{ "<script", "</script", "<iframe", |
︙ | ︙ | |||
99 100 101 102 103 104 105 | attrs.Remove("") attrs = attrs.AddClass("zs-indication").AddClass("zs-" + attrVal) } } return attrs } | < | | | | | < | | < < < < < < < < < < < | < | | | | | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | attrs.Remove("") attrs = attrs.AddClass("zs-indication").AddClass("zs-" + attrVal) } } return attrs } func (v *visitor) visitRegion(rn *ast.RegionNode) { var code string attrs := rn.Attrs oldVerse := v.inVerse switch rn.Kind { case ast.RegionSpan: code = "div" attrs = processSpanAttributes(attrs) case ast.RegionVerse: v.inVerse = true code = "div" case ast.RegionQuote: code = "blockquote" default: panic(fmt.Sprintf("Unknown region kind %v", rn.Kind)) } v.lang.push(attrs) defer v.lang.pop() v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteString(">\n") ast.WalkBlockSlice(v, rn.Blocks) if len(rn.Inlines) > 0 { v.b.WriteString("<cite>") ast.WalkInlineSlice(v, rn.Inlines) v.b.WriteString("</cite>\n") } v.b.WriteStrings("</", code, ">\n") v.inVerse = oldVerse } func (v *visitor) visitHeading(hn *ast.HeadingNode) { v.lang.push(hn.Attrs) defer v.lang.pop() lvl := hn.Level if lvl > 6 { lvl = 6 // HTML has H1..H6 } strLvl := strconv.Itoa(lvl) v.b.WriteStrings("<h", strLvl) v.visitAttributes(hn.Attrs) if slug := hn.Slug; len(slug) > 0 { v.b.WriteStrings(" id=\"", slug, "\"") } v.b.WriteByte('>') ast.WalkInlineSlice(v, hn.Inlines) v.b.WriteStrings("</h", strLvl, ">\n") } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "ol", ast.NestedListUnordered: "ul", } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.lang.push(ln.Attrs) defer v.lang.pop() if ln.Kind == ast.NestedListQuote { // NestedListQuote -> HTML <blockquote> doesn't use <li>...</li> v.writeQuotationList(ln) return } code, ok := mapNestedListKind[ln.Kind] if !ok { panic(fmt.Sprintf("Invalid list kind %v", ln.Kind)) } compact := isCompactList(ln.Items) v.b.WriteStrings("<", code) v.visitAttributes(ln.Attrs) v.b.WriteString(">\n") for _, item := range ln.Items { |
︙ | ︙ | |||
208 209 210 211 212 213 214 | if pn := getParaItem(item); pn != nil { if inPara { v.b.WriteByte('\n') } else { v.b.WriteString("<p>") inPara = true } | | | | 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | if pn := getParaItem(item); pn != nil { if inPara { v.b.WriteByte('\n') } else { v.b.WriteString("<p>") inPara = true } ast.WalkInlineSlice(v, pn.Inlines) } else { if inPara { v.writeEndPara() inPara = false } ast.WalkItemSlice(v, item) } } if inPara { v.writeEndPara() } v.b.WriteString("</blockquote>\n") } |
︙ | ︙ | |||
263 264 265 266 267 268 269 | // writeItemSliceOrPara emits the content of a paragraph if the paragraph is // the only element of the block slice and if compact mode is true. Otherwise, // the item slice is emitted normally. func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) { if compact && len(ins) == 1 { if para, ok := ins[0].(*ast.ParaNode); ok { | | | | | < | | < < | | < | | 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | // writeItemSliceOrPara emits the content of a paragraph if the paragraph is // the only element of the block slice and if compact mode is true. Otherwise, // the item slice is emitted normally. func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) { if compact && len(ins) == 1 { if para, ok := ins[0].(*ast.ParaNode); ok { ast.WalkInlineSlice(v, para.Inlines) return } } ast.WalkItemSlice(v, ins) } func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) { if len(ds) == 1 { if para, ok := ds[0].(*ast.ParaNode); ok { ast.WalkInlineSlice(v, para.Inlines) return } } ast.WalkDescriptionSlice(v, ds) } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { v.b.WriteString("<dl>\n") for _, descr := range dn.Descriptions { v.b.WriteString("<dt>") ast.WalkInlineSlice(v, descr.Term) v.b.WriteString("</dt>\n") for _, b := range descr.Descriptions { v.b.WriteString("<dd>") v.writeDescriptionsSlice(b) v.b.WriteString("</dd>\n") } } v.b.WriteString("</dl>\n") } func (v *visitor) visitTable(tn *ast.TableNode) { v.b.WriteString("<table>\n") if len(tn.Header) > 0 { v.b.WriteString("<thead>\n") v.writeRow(tn.Header, "<th", "</th>") v.b.WriteString("</thead>\n") } if len(tn.Rows) > 0 { |
︙ | ︙ | |||
332 333 334 335 336 337 338 | v.b.WriteString("<tr>") for _, cell := range row { v.b.WriteString(cellStart) if len(cell.Inlines) == 0 { v.b.WriteByte('>') } else { v.b.WriteString(alignStyle[cell.Align]) | | < | | 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | v.b.WriteString("<tr>") for _, cell := range row { v.b.WriteString(cellStart) if len(cell.Inlines) == 0 { v.b.WriteByte('>') } else { v.b.WriteString(alignStyle[cell.Align]) ast.WalkInlineSlice(v, cell.Inlines) } v.b.WriteString(cellEnd) } v.b.WriteString("</tr>\n") } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { switch bn.Syntax { case "gif", "jpeg", "png": v.b.WriteStrings("<img src=\"data:image/", bn.Syntax, ";base64,") v.b.WriteBase64(bn.Blob) v.b.WriteString("\" title=\"") v.writeQuotedEscaped(bn.Title) v.b.WriteString("\">\n") |
︙ | ︙ |
Changes to encoder/htmlenc/htmlenc.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { | > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { encoder.Register(api.EncoderHTML, encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &htmlEncoder{env: env} }, }) } type htmlEncoder struct { env *encoder.Environment } |
︙ | ︙ | |||
47 48 49 50 51 52 53 | v.b.WriteStrings("<title>", encfun.MetaAsText(zn.InhMeta, meta.KeyTitle), "</title>") if inhMeta { v.acceptMeta(zn.InhMeta) } else { v.acceptMeta(zn.Meta) } v.b.WriteString("\n</head>\n<body>\n") | | | | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | v.b.WriteStrings("<title>", encfun.MetaAsText(zn.InhMeta, meta.KeyTitle), "</title>") if inhMeta { v.acceptMeta(zn.InhMeta) } else { v.acceptMeta(zn.Meta) } v.b.WriteString("\n</head>\n<body>\n") ast.WalkBlockSlice(v, zn.Ast) v.writeEndnotes() v.b.WriteString("</body>\n</html>") length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as HTML5. func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(he, w) // Write title if title, ok := m.Get(meta.KeyTitle); ok { textEnc := encoder.Create(api.EncoderText, nil) var sb strings.Builder textEnc.WriteInlines(&sb, parser.ParseMetadata(title)) v.b.WriteStrings("<meta name=\"zs-", meta.KeyTitle, "\" content=\"") v.writeQuotedEscaped(sb.String()) v.b.WriteString("\">") } // Write other metadata v.acceptMeta(m) length, err := v.b.Flush() return length, err } func (he *htmlEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, zn.Ast) } // WriteBlocks encodes a block slice. func (he *htmlEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(he, w) ast.WalkBlockSlice(v, bs) v.writeEndnotes() length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (he *htmlEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(he, w) if env := he.env; env != nil { v.inInteractive = env.Interactive } ast.WalkInlineSlice(v, is) length, err := v.b.Flush() return length, err } |
Changes to encoder/htmlenc/inline.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) | < < < < < < < < < < < < < < < < < < < < < < < | < | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { if v.env.IsXHTML() { v.b.WriteString("<br />\n") } else { v.b.WriteString("<br>\n") } } else { v.b.WriteByte('\n') } } func (v *visitor) visitLink(ln *ast.LinkNode) { ln, n := v.env.AdaptLink(ln) if n != nil { ast.Walk(v, n) return } v.lang.push(ln.Attrs) defer v.lang.pop() switch ln.Ref.State { case ast.RefStateSelf, ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased: |
︙ | ︙ | |||
90 91 92 93 94 95 96 | } v.b.WriteString("<a href=\"") v.writeQuotedEscaped(ln.Ref.Value) v.b.WriteByte('"') v.visitAttributes(ln.Attrs) v.b.WriteByte('>') v.inInteractive = true | | | < | | | < | | | < | < | < | | | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | } v.b.WriteString("<a href=\"") v.writeQuotedEscaped(ln.Ref.Value) v.b.WriteByte('"') v.visitAttributes(ln.Attrs) v.b.WriteByte('>') v.inInteractive = true ast.WalkInlineSlice(v, ln.Inlines) v.inInteractive = false v.b.WriteString("</a>") } } func (v *visitor) writeAHref(ref *ast.Reference, attrs *ast.Attributes, ins ast.InlineSlice) { if v.env.IsInteractive(v.inInteractive) { v.writeSpan(ins, attrs) return } v.b.WriteString("<a href=\"") v.writeReference(ref) v.b.WriteByte('"') v.visitAttributes(attrs) v.b.WriteByte('>') v.inInteractive = true ast.WalkInlineSlice(v, ins) v.inInteractive = false v.b.WriteString("</a>") } func (v *visitor) visitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { ast.Walk(v, n) return } v.lang.push(in.Attrs) defer v.lang.pop() if in.Ref == nil { v.b.WriteString("<img src=\"data:image/") switch in.Syntax { case "svg": v.b.WriteString("svg+xml;utf8,") v.writeQuotedEscaped(string(in.Blob)) default: v.b.WriteStrings(in.Syntax, ";base64,") v.b.WriteBase64(in.Blob) } } else { v.b.WriteString("<img src=\"") v.writeReference(in.Ref) } v.b.WriteString("\" alt=\"") ast.WalkInlineSlice(v, in.Inlines) v.b.WriteByte('"') v.visitAttributes(in.Attrs) if v.env.IsXHTML() { v.b.WriteString(" />") } else { v.b.WriteByte('>') } } func (v *visitor) visitCite(cn *ast.CiteNode) { cn, n := v.env.AdaptCite(cn) if n != nil { ast.Walk(v, n) return } if cn == nil { return } v.lang.push(cn.Attrs) defer v.lang.pop() v.b.WriteString(cn.Key) if len(cn.Inlines) > 0 { v.b.WriteString(", ") ast.WalkInlineSlice(v, cn.Inlines) } } func (v *visitor) visitFootnote(fn *ast.FootnoteNode) { v.lang.push(fn.Attrs) defer v.lang.pop() if v.env.IsInteractive(v.inInteractive) { return } n := strconv.Itoa(v.env.AddFootnote(fn)) v.b.WriteStrings("<sup id=\"fnref:", n, "\"><a href=\"#fn:", n, "\" class=\"zs-footnote-ref\" role=\"doc-noteref\">", n, "</a></sup>") // TODO: what to do with Attrs? } func (v *visitor) visitMark(mn *ast.MarkNode) { if v.env.IsInteractive(v.inInteractive) { return } if len(mn.Text) > 0 { v.b.WriteStrings("<a id=\"", mn.Text, "\"></a>") } } func (v *visitor) visitFormat(fn *ast.FormatNode) { v.lang.push(fn.Attrs) defer v.lang.pop() var code string attrs := fn.Attrs switch fn.Kind { case ast.FormatItalic: code = "i" case ast.FormatEmph: code = "em" case ast.FormatBold: code = "b" case ast.FormatStrong: |
︙ | ︙ | |||
231 232 233 234 235 236 237 | case ast.FormatMonospace: code = "span" attrs = attrs.Set("style", "font-family:monospace") case ast.FormatQuote: v.visitQuotes(fn) return default: | | | | | 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | case ast.FormatMonospace: code = "span" attrs = attrs.Set("style", "font-family:monospace") case ast.FormatQuote: v.visitQuotes(fn) return default: panic(fmt.Sprintf("Unknown format kind %v", fn.Kind)) } v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteByte('>') ast.WalkInlineSlice(v, fn.Inlines) v.b.WriteStrings("</", code, ">") } func (v *visitor) writeSpan(ins ast.InlineSlice, attrs *ast.Attributes) { v.b.WriteString("<span") v.visitAttributes(attrs) v.b.WriteByte('>') ast.WalkInlineSlice(v, ins) v.b.WriteString("</span>") } var langQuotes = map[string][2]string{ meta.ValueLangEN: {"“", "”"}, "de": {"„", "“"}, |
︙ | ︙ | |||
277 278 279 280 281 282 283 | if withSpan { v.b.WriteString("<span") v.visitAttributes(fn.Attrs) v.b.WriteByte('>') } openingQ, closingQ := getQuotes(v.lang.top()) v.b.WriteString(openingQ) | | < | | | | 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | if withSpan { v.b.WriteString("<span") v.visitAttributes(fn.Attrs) v.b.WriteByte('>') } openingQ, closingQ := getQuotes(v.lang.top()) v.b.WriteString(openingQ) ast.WalkInlineSlice(v, fn.Inlines) v.b.WriteString(closingQ) if withSpan { v.b.WriteString("</span>") } } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralProg: v.writeLiteral("<code", "</code>", ln.Attrs, ln.Text) case ast.LiteralKeyb: v.writeLiteral("<kbd", "</kbd>", ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteString("<!-- ") v.writeHTMLEscaped(ln.Text) // writeCommentEscaped v.b.WriteString(" -->") case ast.LiteralHTML: if !ignoreHTMLText(ln.Text) { v.b.WriteString(ln.Text) } default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } func (v *visitor) writeLiteral(codeS, codeE string, attrs *ast.Attributes, text string) { oldVisible := v.visibleSpace if attrs != nil { v.visibleSpace = attrs.HasDefault() |
︙ | ︙ |
Changes to encoder/htmlenc/langstack_test.go.
1 | //----------------------------------------------------------------------------- | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "testing" "zettelstore.de/z/ast" ) func TestStackSimple(t *testing.T) { t.Parallel() exp := "de" s := newLangStack(exp) if got := s.top(); got != exp { t.Errorf("Init: expected %q, but got %q", exp, got) return } |
︙ | ︙ |
Changes to encoder/htmlenc/visitor.go.
︙ | ︙ | |||
40 41 42 43 44 45 46 47 48 49 50 51 52 53 | } return &visitor{ env: he.env, b: encoder.NewBufWriter(w), lang: newLangStack(lang), } } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta) { | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | } return &visitor{ env: he.env, b: encoder.NewBufWriter(w), lang: newLangStack(lang), } } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: v.b.WriteString("<p>") ast.WalkInlineSlice(v, n.Inlines) v.writeEndPara() case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("<hr") v.visitAttributes(n.Attrs) if v.env.IsXHTML() { v.b.WriteString(" />\n") } else { v.b.WriteString(">\n") } case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.writeHTMLEscaped(n.Text) case *ast.TagNode: // TODO: erst mal als span. Link wäre gut, muss man vermutlich via Callback lösen. v.b.WriteString("<span class=\"zettel-tag\">#") v.writeHTMLEscaped(n.Tag) v.b.WriteString("</span>") case *ast.SpaceNode: if v.inVerse || v.env.IsXHTML() { v.b.WriteString(n.Lexeme) } else { v.b.WriteByte(' ') } case *ast.BreakNode: v.visitBreak(n) case *ast.LinkNode: v.visitLink(n) case *ast.ImageNode: v.visitImage(n) case *ast.CiteNode: v.visitCite(n) case *ast.FootnoteNode: v.visitFootnote(n) case *ast.MarkNode: v.visitMark(n) case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta) { |
︙ | ︙ | |||
81 82 83 84 85 86 87 | func (v *visitor) writeMeta(prefix, key, value string) { v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"") v.writeQuotedEscaped(value) v.b.WriteString("\">") } | < < < < < < < < < < < < < < < < | | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | func (v *visitor) writeMeta(prefix, key, value string) { v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"") v.writeQuotedEscaped(value) v.b.WriteString("\">") } func (v *visitor) writeEndnotes() { footnotes := v.env.GetCleanFootnotes() if len(footnotes) > 0 { v.b.WriteString("<ol class=\"zs-endnotes\">\n") for i := 0; i < len(footnotes); i++ { // Do not use a range loop above, because a footnote may contain // a footnote. Therefore v.enc.footnote may grow during the loop. fn := footnotes[i] n := strconv.Itoa(i + 1) v.b.WriteStrings("<li id=\"fn:", n, "\" role=\"doc-endnote\">") ast.WalkInlineSlice(v, fn.Inlines) v.b.WriteStrings( " <a href=\"#fnref:", n, "\" class=\"zs-footnote-backref\" role=\"doc-backlink\">↩︎</a></li>\n") } v.b.WriteString("</ol>\n") } |
︙ | ︙ |
Changes to encoder/jsonenc/djsonenc.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" ) func init() { | > | | | | | | < | > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < | | | | < | < | | < | | | | | | < < | | < < < < < < < < | < | | < > | > > > | | < | < | < | > > > > > | < | < | < | < < | < < | < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" ) func init() { encoder.Register(api.EncoderDJSON, encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &jsonDetailEncoder{env: env} }, }) } type jsonDetailEncoder struct { env *encoder.Environment } // WriteZettel writes the encoded zettel to the writer. func (je *jsonDetailEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newDetailVisitor(w, je) v.b.WriteString("{\"meta\":{\"title\":") v.walkInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle)) if inhMeta { v.writeMeta(zn.InhMeta) } else { v.writeMeta(zn.Meta) } v.b.WriteByte('}') v.b.WriteString(",\"content\":") v.walkBlockSlice(zn.Ast) v.b.WriteByte('}') length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as JSON. func (je *jsonDetailEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newDetailVisitor(w, je) v.b.WriteString("{\"title\":") v.walkInlineSlice(encfun.MetaAsInlineSlice(m, meta.KeyTitle)) v.writeMeta(m) v.b.WriteByte('}') length, err := v.b.Flush() return length, err } func (je *jsonDetailEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return je.WriteBlocks(w, zn.Ast) } // WriteBlocks writes a block slice to the writer func (je *jsonDetailEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newDetailVisitor(w, je) v.walkBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (je *jsonDetailEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newDetailVisitor(w, je) v.walkInlineSlice(is) length, err := v.b.Flush() return length, err } // detailVisitor writes the abstract syntax tree to an io.Writer. type detailVisitor struct { b encoder.BufWriter env *encoder.Environment } func newDetailVisitor(w io.Writer, je *jsonDetailEncoder) *detailVisitor { return &detailVisitor{b: encoder.NewBufWriter(w), env: je.env} } func (v *detailVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: v.writeNodeStart("Para") v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.writeNodeStart("Hrule") v.visitAttributes(n.Attrs) case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.writeNodeStart("Blob") v.writeContentStart('q') writeEscaped(&v.b, n.Title) v.writeContentStart('s') writeEscaped(&v.b, n.Syntax) v.writeContentStart('o') v.b.WriteBase64(n.Blob) v.b.WriteByte('"') case *ast.TextNode: v.writeNodeStart("Text") v.writeContentStart('s') writeEscaped(&v.b, n.Text) case *ast.TagNode: v.writeNodeStart("Tag") v.writeContentStart('s') writeEscaped(&v.b, n.Tag) case *ast.SpaceNode: v.writeNodeStart("Space") if l := len(n.Lexeme); l > 1 { v.writeContentStart('n') v.b.WriteString(strconv.Itoa(l)) } case *ast.BreakNode: if n.Hard { v.writeNodeStart("Hard") } else { v.writeNodeStart("Soft") } case *ast.LinkNode: n, n2 := v.env.AdaptLink(n) if n2 != nil { ast.Walk(v, n2) return nil } v.writeNodeStart("Link") v.visitAttributes(n.Attrs) v.writeContentStart('q') writeEscaped(&v.b, mapRefState[n.Ref.State]) v.writeContentStart('s') writeEscaped(&v.b, n.Ref.String()) v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.ImageNode: v.visitImage(n) case *ast.CiteNode: v.writeNodeStart("Cite") v.visitAttributes(n.Attrs) v.writeContentStart('s') writeEscaped(&v.b, n.Key) if len(n.Inlines) > 0 { v.writeContentStart('i') v.walkInlineSlice(n.Inlines) } case *ast.FootnoteNode: v.writeNodeStart("Footnote") v.visitAttributes(n.Attrs) v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.MarkNode: v.writeNodeStart("Mark") if len(n.Text) > 0 { v.writeContentStart('s') writeEscaped(&v.b, n.Text) } case *ast.FormatNode: v.writeNodeStart(mapFormatKind[n.Kind]) v.visitAttributes(n.Attrs) v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.LiteralNode: kind, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown literal kind %v", n.Kind)) } v.writeNodeStart(kind) v.visitAttributes(n.Attrs) v.writeContentStart('s') writeEscaped(&v.b, n.Text) default: return v } v.b.WriteByte('}') return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimProg: "CodeBlock", ast.VerbatimComment: "CommentBlock", ast.VerbatimHTML: "HTMLBlock", } func (v *detailVisitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind)) } v.writeNodeStart(kind) v.visitAttributes(vn.Attrs) v.writeContentStart('l') for i, line := range vn.Lines { v.writeComma(i) writeEscaped(&v.b, line) } v.b.WriteByte(']') } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "SpanBlock", ast.RegionQuote: "QuoteBlock", ast.RegionVerse: "VerseBlock", } func (v *detailVisitor) visitRegion(rn *ast.RegionNode) { kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %v", rn.Kind)) } v.writeNodeStart(kind) v.visitAttributes(rn.Attrs) v.writeContentStart('b') v.walkBlockSlice(rn.Blocks) if len(rn.Inlines) > 0 { v.writeContentStart('i') v.walkInlineSlice(rn.Inlines) } } func (v *detailVisitor) visitHeading(hn *ast.HeadingNode) { v.writeNodeStart("Heading") v.visitAttributes(hn.Attrs) v.writeContentStart('n') v.b.WriteString(strconv.Itoa(hn.Level)) if slug := hn.Slug; len(slug) > 0 { v.writeContentStart('s') v.b.WriteStrings("\"", slug, "\"") } v.writeContentStart('i') v.walkInlineSlice(hn.Inlines) } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "OrderedList", ast.NestedListUnordered: "BulletList", ast.NestedListQuote: "QuoteList", } func (v *detailVisitor) visitNestedList(ln *ast.NestedListNode) { v.writeNodeStart(mapNestedListKind[ln.Kind]) v.writeContentStart('c') for i, item := range ln.Items { v.writeComma(i) v.b.WriteByte('[') for j, in := range item { v.writeComma(j) ast.Walk(v, in) } v.b.WriteByte(']') } v.b.WriteByte(']') } func (v *detailVisitor) visitDescriptionList(dn *ast.DescriptionListNode) { v.writeNodeStart("DescriptionList") v.writeContentStart('g') for i, def := range dn.Descriptions { v.writeComma(i) v.b.WriteByte('[') v.walkInlineSlice(def.Term) if len(def.Descriptions) > 0 { for _, b := range def.Descriptions { v.b.WriteString(",[") for j, dn := range b { v.writeComma(j) ast.Walk(v, dn) } v.b.WriteByte(']') } } v.b.WriteByte(']') } v.b.WriteByte(']') } func (v *detailVisitor) visitTable(tn *ast.TableNode) { v.writeNodeStart("Table") v.writeContentStart('p') // Table header v.b.WriteByte('[') for i, cell := range tn.Header { v.writeComma(i) v.writeCell(cell) } v.b.WriteString("],") // Table rows v.b.WriteByte('[') for i, row := range tn.Rows { v.writeComma(i) v.b.WriteByte('[') for j, cell := range row { v.writeComma(j) v.writeCell(cell) } v.b.WriteByte(']') } v.b.WriteString("]]") } var alignmentCode = map[ast.Alignment]string{ ast.AlignDefault: "[\"\",", ast.AlignLeft: "[\"<\",", ast.AlignCenter: "[\":\",", ast.AlignRight: "[\">\",", } func (v *detailVisitor) writeCell(cell *ast.TableCell) { v.b.WriteString(alignmentCode[cell.Align]) v.walkInlineSlice(cell.Inlines) v.b.WriteByte(']') } var mapRefState = map[ast.RefState]string{ ast.RefStateInvalid: "invalid", ast.RefStateZettel: "zettel", ast.RefStateSelf: "self", ast.RefStateFound: "zettel", ast.RefStateBroken: "broken", ast.RefStateHosted: "local", ast.RefStateBased: "based", ast.RefStateExternal: "external", } func (v *detailVisitor) visitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { ast.Walk(v, n) return } v.writeNodeStart("Image") v.visitAttributes(in.Attrs) if in.Ref == nil { v.writeContentStart('j') v.b.WriteString("\"s\":") |
︙ | ︙ | |||
358 359 360 361 362 363 364 | v.b.WriteByte('}') } else { v.writeContentStart('s') writeEscaped(&v.b, in.Ref.String()) } if len(in.Inlines) > 0 { v.writeContentStart('i') | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < | < < < < < < < < < < < < < | < < < < < < < | < < < < < < < < < < | < < < < < | < < | < | < | | 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 | v.b.WriteByte('}') } else { v.writeContentStart('s') writeEscaped(&v.b, in.Ref.String()) } if len(in.Inlines) > 0 { v.writeContentStart('i') v.walkInlineSlice(in.Inlines) } } var mapFormatKind = map[ast.FormatKind]string{ ast.FormatItalic: "Italic", ast.FormatEmph: "Emph", ast.FormatBold: "Bold", ast.FormatStrong: "Strong", ast.FormatMonospace: "Mono", ast.FormatStrike: "Strikethrough", ast.FormatDelete: "Delete", ast.FormatUnder: "Underline", ast.FormatInsert: "Insert", ast.FormatSuper: "Super", ast.FormatSub: "Sub", ast.FormatQuote: "Quote", ast.FormatQuotation: "Quotation", ast.FormatSmall: "Small", ast.FormatSpan: "Span", } var mapLiteralKind = map[ast.LiteralKind]string{ ast.LiteralProg: "Code", ast.LiteralKeyb: "Input", ast.LiteralOutput: "Output", ast.LiteralComment: "Comment", ast.LiteralHTML: "HTML", } func (v *detailVisitor) walkBlockSlice(bns ast.BlockSlice) { v.b.WriteByte('[') for i, bn := range bns { v.writeComma(i) ast.Walk(v, bn) } v.b.WriteByte(']') } func (v *detailVisitor) walkInlineSlice(ins ast.InlineSlice) { v.b.WriteByte('[') for i, in := range ins { v.writeComma(i) ast.Walk(v, in) } v.b.WriteByte(']') } // visitAttributes write JSON attributes func (v *detailVisitor) visitAttributes(a *ast.Attributes) { if a == nil || len(a.Attrs) == 0 { |
︙ | ︙ | |||
559 560 561 562 563 564 565 | } } } func (v *detailVisitor) writeSetValue(value string) { v.b.WriteByte('[') for i, val := range meta.ListFromValue(value) { | < | < > > > > > > | 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | } } } func (v *detailVisitor) writeSetValue(value string) { v.b.WriteByte('[') for i, val := range meta.ListFromValue(value) { v.writeComma(i) v.b.WriteByte('"') v.b.Write(Escape(val)) v.b.WriteByte('"') } v.b.WriteByte(']') } func (v *detailVisitor) writeComma(pos int) { if pos > 0 { v.b.WriteByte(',') } } |
Changes to encoder/jsonenc/jsonenc.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package jsonenc encodes the abstract syntax tree into some JSON formats. package jsonenc import ( "bytes" "io" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { | > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Package jsonenc encodes the abstract syntax tree into some JSON formats. package jsonenc import ( "bytes" "io" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderJSON, encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &jsonEncoder{} }, Default: true, }) } // jsonEncoder is just a stub. It is not implemented. The real implementation // is in file web/adapter/json.go |
︙ | ︙ |
Changes to encoder/nativeenc/nativeenc.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { | > | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { encoder.Register(api.EncoderNative, encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &nativeEncoder{env: env} }, }) } type nativeEncoder struct { env *encoder.Environment } // WriteZettel encodes the zettel to the writer. func (ne *nativeEncoder) WriteZettel( w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newVisitor(w, ne) v.b.WriteString("[Title ") v.walkInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle)) v.b.WriteByte(']') if inhMeta { v.acceptMeta(zn.InhMeta, false) } else { v.acceptMeta(zn.Meta, false) } v.b.WriteByte('\n') v.walkBlockSlice(zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data in native format. func (ne *nativeEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(w, ne) v.acceptMeta(m, true) length, err := v.b.Flush() return length, err } func (ne *nativeEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ne.WriteBlocks(w, zn.Ast) } // WriteBlocks writes a block slice to the writer func (ne *nativeEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(w, ne) v.walkBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (ne *nativeEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(w, ne) v.walkInlineSlice(is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter level int env *encoder.Environment } func newVisitor(w io.Writer, enc *nativeEncoder) *visitor { return &visitor{b: encoder.NewBufWriter(w), env: enc.env} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: v.b.WriteString("[Para ") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.b.WriteStrings("[Heading ", strconv.Itoa(n.Level), " \"", n.Slug, "\"") v.visitAttributes(n.Attrs) v.b.WriteByte(' ') v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.HRuleNode: v.b.WriteString("[Hrule") v.visitAttributes(n.Attrs) v.b.WriteByte(']') case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.b.WriteString("[BLOB \"") v.writeEscaped(n.Title) v.b.WriteString("\" \"") v.writeEscaped(n.Syntax) v.b.WriteString("\" \"") v.b.WriteBase64(n.Blob) v.b.WriteString("\"]") case *ast.TextNode: v.b.WriteString("Text \"") v.writeEscaped(n.Text) v.b.WriteByte('"') case *ast.TagNode: v.b.WriteString("Tag \"") v.writeEscaped(n.Tag) v.b.WriteByte('"') case *ast.SpaceNode: v.b.WriteString("Space") if l := len(n.Lexeme); l > 1 { v.b.WriteByte(' ') v.b.WriteString(strconv.Itoa(l)) } case *ast.BreakNode: if n.Hard { v.b.WriteString("Break") } else { v.b.WriteString("Space") } case *ast.LinkNode: v.visitLink(n) case *ast.ImageNode: v.visitImage(n) case *ast.CiteNode: v.b.WriteString("Cite") v.visitAttributes(n.Attrs) v.b.WriteString(" \"") v.writeEscaped(n.Key) v.b.WriteByte('"') if len(n.Inlines) > 0 { v.b.WriteString(" [") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') } case *ast.FootnoteNode: v.b.WriteString("Footnote") v.visitAttributes(n.Attrs) v.b.WriteString(" [") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.MarkNode: v.b.WriteString("Mark") if len(n.Text) > 0 { v.b.WriteString(" \"") v.writeEscaped(n.Text) v.b.WriteByte('"') } case *ast.FormatNode: v.b.Write(mapFormatKind[n.Kind]) v.visitAttributes(n.Attrs) v.b.WriteString(" [") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.LiteralNode: kind, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown literal kind %v", n.Kind)) } v.b.Write(kind) v.visitAttributes(n.Attrs) v.b.WriteString(" \"") v.writeEscaped(n.Text) v.b.WriteByte('"') default: return v } return nil } var ( rawBackslash = []byte{'\\', '\\'} rawDoubleQuote = []byte{'\\', '"'} rawNewline = []byte{'\\', 'n'} ) func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) { if withTitle { v.b.WriteString("[Title ") v.walkInlineSlice(parser.ParseMetadata(m.GetDefault(meta.KeyTitle, ""))) v.b.WriteByte(']') } v.writeMetaString(m, meta.KeyRole, "Role") v.writeMetaList(m, meta.KeyTags, "Tags") v.writeMetaString(m, meta.KeySyntax, "Syntax") pairs := m.PairsRest(true) if len(pairs) == 0 { return } v.b.WriteString("\n[Header") v.level++ for i, p := range pairs { v.writeComma(i) v.writeNewLine() v.b.WriteByte('[') v.b.WriteStrings(p.Key, " \"") v.writeEscaped(p.Value) v.b.WriteString("\"]") } v.level-- |
︙ | ︙ | |||
139 140 141 142 143 144 145 | v.b.WriteByte(' ') v.b.WriteString(val) } v.b.WriteByte(']') } } | < < < < < < < | < | | | | | < | | | | | | < < < < < < < < < < < < < < < < | < | | < | < > > | > > > > < | < | < | > > > | > > > < | < | < < | < < | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | < | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < | < < < < < < < < < < < < < | | | < < | < < < < < < < < < < < < < < | < < < < < | < | < < | 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 | v.b.WriteByte(' ') v.b.WriteString(val) } v.b.WriteByte(']') } } var mapVerbatimKind = map[ast.VerbatimKind][]byte{ ast.VerbatimProg: []byte("[CodeBlock"), ast.VerbatimComment: []byte("[CommentBlock"), ast.VerbatimHTML: []byte("[HTMLBlock"), } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind)) } v.b.Write(kind) 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 mapRegionKind = map[ast.RegionKind][]byte{ ast.RegionSpan: []byte("[SpanBlock"), ast.RegionQuote: []byte("[QuoteBlock"), ast.RegionVerse: []byte("[VerseBlock"), } func (v *visitor) visitRegion(rn *ast.RegionNode) { kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %v", rn.Kind)) } v.b.Write(kind) v.visitAttributes(rn.Attrs) v.level++ v.writeNewLine() v.b.WriteByte('[') v.level++ v.walkBlockSlice(rn.Blocks) v.level-- v.b.WriteByte(']') if len(rn.Inlines) > 0 { v.b.WriteByte(',') v.writeNewLine() v.b.WriteString("[Cite ") v.walkInlineSlice(rn.Inlines) v.b.WriteByte(']') } v.level-- v.b.WriteByte(']') } var mapNestedListKind = map[ast.NestedListKind][]byte{ ast.NestedListOrdered: []byte("[OrderedList"), ast.NestedListUnordered: []byte("[BulletList"), ast.NestedListQuote: []byte("[QuoteList"), } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.b.Write(mapNestedListKind[ln.Kind]) v.level++ for i, item := range ln.Items { v.writeComma(i) v.writeNewLine() v.level++ v.b.WriteByte('[') for i, in := range item { if i > 0 { v.b.WriteByte(',') v.writeNewLine() } ast.Walk(v, in) } v.b.WriteByte(']') v.level-- } v.level-- v.b.WriteByte(']') } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { v.b.WriteString("[DescriptionList") v.level++ for i, descr := range dn.Descriptions { v.writeComma(i) v.writeNewLine() v.b.WriteString("[Term [") v.walkInlineSlice(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() for i, dn := range b { if i > 0 { v.b.WriteByte(',') v.writeNewLine() } ast.Walk(v, dn) } v.b.WriteByte(']') v.level-- } v.level-- } v.b.WriteByte(']') } v.level-- v.b.WriteByte(']') } 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 { v.writeComma(i) v.writeCell(cell) } v.b.WriteString("],") } for i, row := range tn.Rows { v.writeComma(i) v.writeNewLine() v.b.WriteString("[Row ") for j, cell := range row { v.writeComma(j) 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.walkInlineSlice(cell.Inlines) } v.b.WriteByte(']') } var mapRefState = map[ast.RefState]string{ ast.RefStateInvalid: "INVALID", ast.RefStateZettel: "ZETTEL", ast.RefStateSelf: "SELF", ast.RefStateFound: "ZETTEL", ast.RefStateBroken: "BROKEN", ast.RefStateHosted: "LOCAL", ast.RefStateBased: "BASED", ast.RefStateExternal: "EXTERNAL", } func (v *visitor) visitLink(ln *ast.LinkNode) { ln, n := v.env.AdaptLink(ln) if n != nil { ast.Walk(v, n) 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.walkInlineSlice(ln.Inlines) } v.b.WriteByte(']') } func (v *visitor) visitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { ast.Walk(v, n) 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.walkInlineSlice(in.Inlines) v.b.WriteByte(']') } } var mapFormatKind = map[ast.FormatKind][]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"), } var mapLiteralKind = map[ast.LiteralKind][]byte{ ast.LiteralProg: []byte("Code"), ast.LiteralKeyb: []byte("Input"), ast.LiteralOutput: []byte("Output"), ast.LiteralComment: []byte("Comment"), ast.LiteralHTML: []byte("HTML"), } func (v *visitor) walkBlockSlice(bns ast.BlockSlice) { for i, bn := range bns { if i > 0 { v.b.WriteByte(',') v.writeNewLine() } ast.Walk(v, bn) } } func (v *visitor) walkInlineSlice(ins ast.InlineSlice) { for i, in := range ins { v.writeComma(i) ast.Walk(v, in) } } // 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("\",[") for i, k := range keys { if k == "" { continue } v.writeComma(i) v.b.WriteString(k) val := a.Attrs[k] if len(val) > 0 { v.b.WriteString("=\"") v.writeEscaped(val) v.b.WriteByte('"') } } v.b.WriteString("])") } func (v *visitor) writeNewLine() { v.b.WriteByte('\n') for i := 0; i < v.level; i++ { |
︙ | ︙ | |||
608 609 610 611 612 613 614 | } v.b.WriteString(s[last:i]) v.b.Write(b) last = i + 1 } v.b.WriteString(s[last:]) } | > > > > > > | 561 562 563 564 565 566 567 568 569 570 571 572 573 | } v.b.WriteString(s[last:i]) v.b.Write(b) last = i + 1 } v.b.WriteString(s[last:]) } func (v *visitor) writeComma(pos int) { if pos > 0 { v.b.WriteByte(',') } } |
Changes to encoder/rawenc/rawenc.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 | // 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() { | > | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Package rawenc encodes the abstract syntax tree as raw content. package rawenc import ( "io" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderRaw, encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &rawEncoder{} }, }) } type rawEncoder struct{} // WriteZettel writes the encoded zettel to the writer. |
︙ | ︙ |
Changes to encoder/textenc/textenc.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) func init() { | > | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) func init() { encoder.Register(api.EncoderText, encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &textEncoder{} }, }) } type textEncoder struct{} // WriteZettel writes metadata and content. |
︙ | ︙ | |||
81 82 83 84 85 86 87 | 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) | | < | < < | < | | | | | < | < | | < | < | | | | | | < | < < < < < < < < < | | > | | < | | < | | | | < | < | < | | < > > > | | | | < | | | < < < < < | | | < | < < < | | < < < < | < < < < | | < | < | | < | < | | < | < | | | | | | < | < | | | | < | < | < < | | | < > > | | < < < < > < < < < | < < | | < | < < < < < | < | < < < < | < < < | < | | < < < < < < < | 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | 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) ast.WalkInlineSlice(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewBufWriter(w)} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.VerbatimNode: if n.Kind == ast.VerbatimComment { return nil } for i, line := range n.Lines { v.writePosChar(i, '\n') v.b.WriteString(line) } return nil case *ast.RegionNode: v.acceptBlockSlice(n.Blocks) if len(n.Inlines) > 0 { v.b.WriteByte('\n') ast.WalkInlineSlice(v, n.Inlines) } return nil case *ast.NestedListNode: for i, item := range n.Items { v.writePosChar(i, '\n') for j, it := range item { v.writePosChar(j, '\n') ast.Walk(v, it) } } return nil case *ast.DescriptionListNode: for i, descr := range n.Descriptions { v.writePosChar(i, '\n') ast.WalkInlineSlice(v, descr.Term) for _, b := range descr.Descriptions { v.b.WriteByte('\n') for k, d := range b { v.writePosChar(k, '\n') ast.Walk(v, d) } } } return nil case *ast.TableNode: if len(n.Header) > 0 { v.writeRow(n.Header) v.b.WriteByte('\n') } for i, row := range n.Rows { v.writePosChar(i, '\n') v.writeRow(row) } return nil case *ast.TextNode: v.b.WriteString(n.Text) return nil case *ast.TagNode: v.b.WriteStrings("#", n.Tag) return nil case *ast.SpaceNode: v.b.WriteByte(' ') return nil case *ast.BreakNode: if n.Hard { v.b.WriteByte('\n') } else { v.b.WriteByte(' ') } return nil case *ast.LinkNode: if !n.OnlyRef { ast.WalkInlineSlice(v, n.Inlines) } return nil case *ast.FootnoteNode: v.b.WriteByte(' ') return v // No 'return nil' to write text case *ast.LiteralNode: if n.Kind != ast.LiteralComment { v.b.WriteString(n.Text) } } return v } func (v *visitor) writeRow(row ast.TableRow) { for i, cell := range row { v.writePosChar(i, ' ') ast.WalkInlineSlice(v, cell.Inlines) } } func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) { for i, bn := range bns { v.writePosChar(i, '\n') ast.Walk(v, bn) } } func (v *visitor) writePosChar(pos int, ch byte) { if pos > 0 { v.b.WriteByte(ch) } } |
Changes to encoder/zmkenc/zmkenc.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 | package zmkenc import ( "fmt" "io" "sort" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { | > | | | | | > | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | | | | < | | | | | | | < | | < < < < < < < | < | | | < | | < | < < | | | < < < < < < < < < < < | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | package zmkenc import ( "fmt" "io" "sort" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderZmk, encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &zmkEncoder{} }, }) } type zmkEncoder struct{} // WriteZettel writes the encoded zettel to the writer. func (ze *zmkEncoder) WriteZettel( w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newVisitor(w, ze) if inhMeta { zn.InhMeta.WriteAsHeader(&v.b, true) } else { zn.Meta.WriteAsHeader(&v.b, true) } ast.WalkBlockSlice(v, 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) ast.WalkBlockSlice(v, 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) ast.WalkInlineSlice(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter prefix []byte enc *zmkEncoder } func newVisitor(w io.Writer, enc *zmkEncoder) *visitor { return &visitor{ b: encoder.NewBufWriter(w), enc: enc, } } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: ast.WalkInlineSlice(v, n.Inlines) v.b.WriteByte('\n') if len(v.prefix) == 0 { v.b.WriteByte('\n') } case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("---") v.visitAttributes(n.Attrs) v.b.WriteByte('\n') case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.b.WriteStrings( "%% Unable to display BLOB with title '", n.Title, "' and syntax '", n.Syntax, "'\n") case *ast.TextNode: v.visitText(n) case *ast.TagNode: v.b.WriteStrings("#", n.Tag) case *ast.SpaceNode: v.b.WriteString(n.Lexeme) case *ast.BreakNode: v.visitBreak(n) case *ast.LinkNode: v.visitLink(n) case *ast.ImageNode: v.visitImage(n) case *ast.CiteNode: v.visitCite(n) case *ast.FootnoteNode: v.b.WriteString("[^") ast.WalkInlineSlice(v, n.Inlines) v.b.WriteByte(']') v.visitAttributes(n.Attrs) case *ast.MarkNode: v.b.WriteStrings("[!", n.Text, "]") case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } 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 mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", ast.RegionVerse: "\"\"\"", } func (v *visitor) visitRegion(rn *ast.RegionNode) { // Scan rn.Blocks for embedded regions to adjust length of regionCode kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %d", rn.Kind)) } v.b.WriteString(kind) v.visitAttributes(rn.Attrs) v.b.WriteByte('\n') ast.WalkBlockSlice(v, rn.Blocks) v.b.WriteString(kind) if len(rn.Inlines) > 0 { v.b.WriteByte(' ') ast.WalkInlineSlice(v, rn.Inlines) } v.b.WriteByte('\n') } func (v *visitor) visitHeading(hn *ast.HeadingNode) { for i := 0; i <= hn.Level; i++ { v.b.WriteByte('=') } v.b.WriteByte(' ') ast.WalkInlineSlice(v, hn.Inlines) v.visitAttributes(hn.Attrs) v.b.WriteByte('\n') } var mapNestedListKind = map[ast.NestedListKind]byte{ ast.NestedListOrdered: '#', ast.NestedListUnordered: '*', ast.NestedListQuote: '>', } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) for _, 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(' ') } } } ast.Walk(v, in) } } v.prefix = v.prefix[:len(v.prefix)-1] v.b.WriteByte('\n') } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { for _, descr := range dn.Descriptions { v.b.WriteString("; ") ast.WalkInlineSlice(v, descr.Term) v.b.WriteByte('\n') for _, b := range descr.Descriptions { v.b.WriteString(": ") ast.WalkDescriptionSlice(v, b) v.b.WriteByte('\n') } } } var alignCode = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "<", ast.AlignCenter: ":", ast.AlignRight: ">", } 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]) } ast.WalkInlineSlice(v, 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]) } ast.WalkInlineSlice(v, cell.Inlines) } v.b.WriteByte('\n') } v.b.WriteByte('\n') } var escapeSeqs = map[string]bool{ "\\": true, "//": true, "**": true, "__": true, "~~": true, "^^": true, ",,": true, "<<": true, "\"\"": true, ";;": true, "::": true, "''": true, "``": true, "++": true, "==": true, } 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 |
︙ | ︙ | |||
277 278 279 280 281 282 283 | continue } } } v.b.WriteString(tn.Text[last:]) } | < < < < < < < < < < < | < | | < | | < | | < < < < < < < < < < < < < | < | | | | | | | < | | | < < < < < < < < < < < | 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 | continue } } } v.b.WriteString(tn.Text[last:]) } func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteByte('\n') } if prefixLen := len(v.prefix); prefixLen > 0 { for i := 0; i <= prefixLen; i++ { v.b.WriteByte(' ') } } } func (v *visitor) visitLink(ln *ast.LinkNode) { v.b.WriteString("[[") if !ln.OnlyRef { ast.WalkInlineSlice(v, ln.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(ln.Ref.String(), "]]") } func (v *visitor) visitImage(in *ast.ImageNode) { if in.Ref != nil { v.b.WriteString("{{") if len(in.Inlines) > 0 { ast.WalkInlineSlice(v, in.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(in.Ref.String(), "}}") } } func (v *visitor) visitCite(cn *ast.CiteNode) { v.b.WriteStrings("[@", cn.Key) if len(cn.Inlines) > 0 { v.b.WriteString(", ") ast.WalkInlineSlice(v, cn.Inlines) } v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } var mapFormatKind = map[ast.FormatKind][]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("''"), } func (v *visitor) visitFormat(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] if !ok { panic(fmt.Sprintf("Unknown format kind %d", fn.Kind)) } attrs := fn.Attrs switch fn.Kind { case ast.FormatEmph, ast.FormatStrong, ast.FormatInsert, ast.FormatDelete: attrs = attrs.Clone() attrs.Set("-", "") } v.b.Write(kind) ast.WalkInlineSlice(v, fn.Inlines) v.b.Write(kind) v.visitAttributes(attrs) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { 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 kind %v", ln.Kind)) } } 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) } // 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 { |
︙ | ︙ |
Changes to go.mod.
1 2 3 4 5 6 7 | module zettelstore.de/z go 1.16 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 | module zettelstore.de/z go 1.16 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 github.com/yuin/goldmark v1.4.0 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b golang.org/x/text v0.3.6 ) |
Changes to go.sum.
1 2 3 4 | 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= | | | < | | | < | | > | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs= github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A= github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
Changes to input/input_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import ( "testing" "zettelstore.de/z/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", ""}, {"&", "&"}, | > > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | import ( "testing" "zettelstore.de/z/input" ) func TestEatEOL(t *testing.T) { t.Parallel() 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) { t.Parallel() var testcases = []struct { text string exp string }{ {"", ""}, {"a", ""}, {"&", "&"}, |
︙ | ︙ |
Added input/runes.go.
> > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package input provides an abstraction for data to be read. package input // IsSpace returns true if rune is a whitespace. func IsSpace(ch rune) bool { switch ch { case ' ', '\t': return true } return false } |
Added kernel/impl/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "context" "fmt" "io" "net/url" "sync" "zettelstore.de/z/box" "zettelstore.de/z/kernel" ) type boxService struct { srvConfig mxService sync.RWMutex manager box.Manager createManager kernel.CreateBoxManagerFunc } func (ps *boxService) Initialize() { ps.descr = descriptionMap{ kernel.BoxDefaultDirType: { "Default directory box type", ps.noFrozen(func(val string) interface{} { switch val { case kernel.BoxDirTypeNotify, kernel.BoxDirTypeSimple: return val } return nil }), true, }, kernel.BoxURIs: { "Box URI", func(val string) interface{} { uVal, err := url.Parse(val) if err != nil { return nil } if uVal.Scheme == "" { uVal.Scheme = "dir" } return uVal }, true, }, } ps.next = interfaceMap{ kernel.BoxDefaultDirType: kernel.BoxDirTypeNotify, } } func (ps *boxService) Start(kern *myKernel) error { boxURIs := make([]*url.URL, 0, 4) format := kernel.BoxURIs + "%d" for i := 1; ; i++ { u := ps.GetNextConfig(fmt.Sprintf(format, i)) if u == nil { break } boxURIs = append(boxURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, kern.cfg.rtConfig) if err != nil { kern.doLog("Unable to create box manager:", err) return err } kern.doLog("Start Box Manager:", mgr.Location()) if err := mgr.Start(context.Background()); err != nil { kern.doLog("Unable to start box manager:", err) } kern.cfg.setBox(mgr) ps.manager = mgr return nil } func (ps *boxService) IsStarted() bool { ps.mxService.RLock() defer ps.mxService.RUnlock() return ps.manager != nil } func (ps *boxService) Stop(kern *myKernel) error { kern.doLog("Stop Box Manager") ps.mxService.RLock() mgr := ps.manager ps.mxService.RUnlock() err := mgr.Stop(context.Background()) ps.mxService.Lock() ps.manager = nil ps.mxService.Unlock() return err } func (ps *boxService) GetStatistics() []kernel.KeyValue { var st box.Stats ps.mxService.RLock() ps.manager.ReadStats(&st) ps.mxService.RUnlock() return []kernel.KeyValue{ {Key: "Read-only", Value: fmt.Sprintf("%v", st.ReadOnly)}, {Key: "Managed boxes", Value: fmt.Sprintf("%v", st.NumManagedBoxes)}, {Key: "Zettel (total)", Value: fmt.Sprintf("%v", st.ZettelTotal)}, {Key: "Zettel (indexed)", Value: fmt.Sprintf("%v", st.ZettelIndexed)}, {Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")}, {Key: "Duration last re-index", Value: fmt.Sprintf("%vms", st.DurLastReload.Milliseconds())}, {Key: "Indexes since last re-index", Value: fmt.Sprintf("%v", st.IndexesSinceReload)}, {Key: "Indexed words", Value: fmt.Sprintf("%v", st.IndexedWords)}, {Key: "Indexed URLs", Value: fmt.Sprintf("%v", st.IndexedUrls)}, {Key: "Zettel enrichments", Value: fmt.Sprintf("%v", st.IndexUpdates)}, } } func (ps *boxService) DumpIndex(w io.Writer) { ps.manager.Dump(w) } |
Changes to kernel/impl/cfg.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package impl provides the kernel implementation. package impl import ( "context" "fmt" | < > < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // Package impl provides the kernel implementation. package impl import ( "context" "fmt" "strings" "sync" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) type configService struct { srvConfig mxService sync.RWMutex rtConfig *myConfig } |
︙ | ︙ | |||
44 45 46 47 48 49 50 | if vis == meta.VisibilityUnknown { return nil } return vis }, true, }, | | | | < < < < < < < < < < < < | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | if vis == meta.VisibilityUnknown { return nil } return vis }, true, }, meta.KeyExpertMode: {"Expert mode", parseBool, true}, meta.KeyFooterHTML: {"Footer HTML", parseString, true}, meta.KeyHomeZettel: {"Home zettel", parseZid, true}, meta.KeyMarkerExternal: {"Marker external URL", parseString, true}, meta.KeySiteName: {"Site name", parseString, true}, meta.KeyYAMLHeader: {"YAML header", parseBool, true}, meta.KeyZettelFileSyntax: { "Zettel file syntax", func(val string) interface{} { return strings.Fields(val) }, true, }, } cs.next = interfaceMap{ meta.KeyDefaultCopyright: "", meta.KeyDefaultLang: meta.ValueLangEN, meta.KeyDefaultRole: meta.ValueRoleZettel, meta.KeyDefaultSyntax: meta.ValueSyntaxZmk, meta.KeyDefaultTitle: "Untitled", meta.KeyDefaultVisibility: meta.VisibilityLogin, meta.KeyExpertMode: false, meta.KeyFooterHTML: "", meta.KeyHomeZettel: id.DefaultHomeZid, meta.KeyMarkerExternal: "➚", meta.KeySiteName: "Zettelstore", meta.KeyYAMLHeader: false, meta.KeyZettelFileSyntax: nil, } } |
︙ | ︙ | |||
115 116 117 118 119 120 121 | return nil } func (cs *configService) GetStatistics() []kernel.KeyValue { return nil } | | | | | | | | | 102 103 104 105 106 107 108 109 110 111 112 113 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 | return nil } func (cs *configService) GetStatistics() []kernel.KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { cs.rtConfig.setBox(mgr) } // myConfig contains all runtime configuration data relevant for the software. type myConfig struct { mx sync.RWMutex orig *meta.Meta data *meta.Meta } // New creates a new Config value. func newConfig(orig *meta.Meta) *myConfig { cfg := myConfig{ orig: orig, data: orig.Clone(), } return &cfg } func (cfg *myConfig) setBox(mgr box.Manager) { mgr.RegisterObserver(cfg.observe) cfg.doUpdate(mgr) } func (cfg *myConfig) doUpdate(p box.Box) error { m, err := p.GetMeta(context.Background(), cfg.data.Zid) if err != nil { return err } cfg.mx.Lock() for _, pair := range cfg.data.Pairs(false) { if val, ok := m.Get(pair.Key); ok { cfg.data.Set(pair.Key, val) } } cfg.mx.Unlock() return nil } func (cfg *myConfig) observe(ci box.UpdateInfo) { if ci.Reason == box.OnReload || ci.Zid == id.ConfigurationZid { go func() { cfg.doUpdate(ci.Box) }() } } var defaultKeys = map[string]string{ meta.KeyCopyright: meta.KeyDefaultCopyright, meta.KeyLang: meta.KeyDefaultLang, meta.KeyLicense: meta.KeyDefaultLicense, |
︙ | ︙ | |||
178 179 180 181 182 183 184 | if cfg == nil { return m } result := m cfg.mx.RLock() for k, d := range defaultKeys { if _, ok := result.Get(k); !ok { | > | | | < | 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | if cfg == nil { return m } result := m cfg.mx.RLock() for k, d := range defaultKeys { if _, ok := result.Get(k); !ok { if val, ok := cfg.data.Get(d); ok && val != "" { if result == m { result = m.Clone() } result.Set(k, val) } } } cfg.mx.RUnlock() return result } |
︙ | ︙ | |||
256 257 258 259 260 261 262 | return cfg.getString(meta.KeyMarkerExternal) } // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(meta.KeyFooterHTML) } | < < < < < < < < < < < < < | 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | return cfg.getString(meta.KeyMarkerExternal) } // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(meta.KeyFooterHTML) } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cfg *myConfig) GetZettelFileSyntax() []string { cfg.mx.RLock() defer cfg.mx.RUnlock() return cfg.data.GetListOrNil(meta.KeyZettelFileSyntax) } |
︙ | ︙ |
Changes to kernel/impl/cmd.go.
︙ | ︙ | |||
62 63 64 65 66 67 68 69 70 71 72 73 74 75 | for _, arg := range args[1:] { io.WriteString(sess.w, " ") io.WriteString(sess.w, arg) } } sess.w.Write(sess.eol) } func (sess *cmdSession) printTable(table [][]string) { maxLen := sess.calcMaxLen(table) if len(maxLen) == 0 { return } if sess.header { | > > > > | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | for _, arg := range args[1:] { io.WriteString(sess.w, " ") io.WriteString(sess.w, arg) } } sess.w.Write(sess.eol) } func (sess *cmdSession) usage(cmd, val string) { sess.println("Usage:", cmd, val) } func (sess *cmdSession) printTable(table [][]string) { maxLen := sess.calcMaxLen(table) if len(maxLen) == 0 { return } if sess.header { |
︙ | ︙ | |||
277 278 279 280 281 282 283 | table = append(table, []string{kdv.Key, kdv.Value, kdv.Descr}) } sess.printTable(table) } func cmdSetConfig(sess *cmdSession, cmd string, args []string) bool { if len(args) < 3 { | | | 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | table = append(table, []string{kdv.Key, kdv.Value, kdv.Descr}) } sess.printTable(table) } func cmdSetConfig(sess *cmdSession, cmd string, args []string) bool { if len(args) < 3 { sess.usage(cmd, "SERVICE KEY VALUE") return true } srvD, ok := sess.kern.srvNames[args[0]] if !ok { sess.println("Unknown service:", args[0]) return true } |
︙ | ︙ | |||
349 350 351 352 353 354 355 | sess.println(err.Error()) } return true } func cmdStat(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { | | | 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | sess.println(err.Error()) } return true } func cmdStat(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.usage(cmd, "SERVICE") return true } srvD, ok := sess.kern.srvNames[args[0]] if !ok { sess.println("Unknown service", args[0]) return true } |
︙ | ︙ | |||
371 372 373 374 375 376 377 | } sess.printTable(table) return true } func lookupService(sess *cmdSession, cmd string, args []string) (kernel.Service, bool) { if len(args) == 0 { | | | 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 | } sess.printTable(table) return true } func lookupService(sess *cmdSession, cmd string, args []string) (kernel.Service, bool) { if len(args) == 0 { sess.usage(cmd, "SERVICE") return 0, false } srvD, ok := sess.kern.srvNames[args[0]] if !ok { sess.println("Unknown service", args[0]) return 0, false } |
︙ | ︙ | |||
430 431 432 433 434 435 436 | func cmdDumpIndex(sess *cmdSession, cmd string, args []string) bool { sess.kern.DumpIndex(sess.w) return true } func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { | | | 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | func cmdDumpIndex(sess *cmdSession, cmd string, args []string) bool { sess.kern.DumpIndex(sess.w) return true } func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.usage(cmd, "RECOVER") sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.") return true } lines := sess.kern.core.RecoverLines(args[0]) if len(lines) == 0 { return true } |
︙ | ︙ |
Changes to kernel/impl/config.go.
︙ | ︙ | |||
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 | func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue { return cfg.getConfigList(true, cfg.GetNextConfig) } func (cfg *srvConfig) getConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue { if len(cfg.descr) == 0 { return nil } keys := make([]string, 0, len(cfg.descr)) for k, descr := range cfg.descr { if all || descr.canList { if !strings.HasSuffix(k, "-") { keys = append(keys, k) continue } format := k + "%d" for i := 1; ; i++ { key := fmt.Sprintf(format, i) val := getConfig(key) if val == nil { break } keys = append(keys, key) } } } sort.Strings(keys) | > > > > > > > > > > > > > > > > > > > > > < < < < < < < < < < < < < < < < | | 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 | func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue { return cfg.getConfigList(true, cfg.GetNextConfig) } func (cfg *srvConfig) getConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue { if len(cfg.descr) == 0 { return nil } keys := cfg.getSortedConfigKeys(all, getConfig) result := make([]kernel.KeyDescrValue, 0, len(keys)) for _, k := range keys { val := getConfig(k) if val == nil { continue } descr, ok := cfg.descr[k] if !ok { descr, _, _ = cfg.getListDescription(k) } result = append(result, kernel.KeyDescrValue{ Key: k, Descr: descr.text, Value: fmt.Sprintf("%v", val), }) } return result } func (cfg *srvConfig) getSortedConfigKeys(all bool, getConfig func(string) interface{}) []string { keys := make([]string, 0, len(cfg.descr)) for k, descr := range cfg.descr { if all || descr.canList { if !strings.HasSuffix(k, "-") { keys = append(keys, k) continue } format := k + "%d" for i := 1; ; i++ { key := fmt.Sprintf(format, i) val := getConfig(key) if val == nil { break } keys = append(keys, key) } } } sort.Strings(keys) return keys } func (cfg *srvConfig) Freeze() { cfg.mxConfig.Lock() cfg.frozen = true cfg.mxConfig.Unlock() } |
︙ | ︙ |
Changes to kernel/impl/impl.go.
︙ | ︙ | |||
30 31 32 33 34 35 36 | type myKernel struct { // started bool wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal debug bool | | | | | | | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | type myKernel struct { // started bool wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal debug bool core coreService cfg configService auth authService box boxService web webService srvs map[kernel.Service]serviceDescr srvNames map[string]serviceData depStart serviceDependency depStop serviceDependency // reverse of depStart } |
︙ | ︙ | |||
66 67 68 69 70 71 72 | kern := &myKernel{ interrupt: make(chan os.Signal, 5), } kern.srvs = map[kernel.Service]serviceDescr{ kernel.CoreService: {&kern.core, "core"}, kernel.ConfigService: {&kern.cfg, "config"}, kernel.AuthService: {&kern.auth, "auth"}, | | | | | | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | kern := &myKernel{ interrupt: make(chan os.Signal, 5), } kern.srvs = map[kernel.Service]serviceDescr{ kernel.CoreService: {&kern.core, "core"}, kernel.ConfigService: {&kern.cfg, "config"}, kernel.AuthService: {&kern.auth, "auth"}, kernel.BoxService: {&kern.box, "box"}, kernel.WebService: {&kern.web, "web"}, } kern.srvNames = make(map[string]serviceData, len(kern.srvs)) for key, srvD := range kern.srvs { if _, ok := kern.srvNames[srvD.name]; ok { panic(fmt.Sprintf("Key %q already given for service %v", key, srvD.name)) } kern.srvNames[srvD.name] = serviceData{srvD.srv, key} srvD.srv.Initialize() } kern.depStart = serviceDependency{ kernel.CoreService: nil, kernel.ConfigService: {kernel.CoreService}, kernel.AuthService: {kernel.CoreService}, kernel.BoxService: {kernel.CoreService, kernel.ConfigService, kernel.AuthService}, kernel.WebService: {kernel.ConfigService, kernel.AuthService, kernel.BoxService}, } kern.depStop = make(serviceDependency, len(kern.depStart)) for srv, deps := range kern.depStart { for _, dep := range deps { kern.depStop[dep] = append(kern.depStop[dep], srv) } } return kern } func (kern *myKernel) Start(headline bool, lineServer bool) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } kern.wg.Add(1) signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM) go func() { // Wait for interrupt. |
︙ | ︙ | |||
124 125 126 127 128 129 130 | kern.core.GetConfig(kernel.CoreGoArch), )) kern.doLog("Licensed under the latest version of the EUPL (European Union Public License)") if kern.auth.GetConfig(kernel.AuthReadonly).(bool) { kern.doLog("Read-only mode") } } | > | | | | > | 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | kern.core.GetConfig(kernel.CoreGoArch), )) kern.doLog("Licensed under the latest version of the EUPL (European Union Public License)") if kern.auth.GetConfig(kernel.AuthReadonly).(bool) { kern.doLog("Read-only mode") } } if lineServer { port := kern.core.GetNextConfig(kernel.CorePort).(int) if port > 0 { listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) startLineServer(kern, listenAddr) } } } func (kern *myKernel) shutdown() { kern.StopService(kernel.CoreService) // Will stop all other services. } |
︙ | ︙ | |||
303 304 305 306 307 308 309 | found[depSrv] = true } } } return append(result, srvD.srv) } func (kern *myKernel) DumpIndex(w io.Writer) { | | | 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | found[depSrv] = true } } } return append(result, srvD.srv) } func (kern *myKernel) DumpIndex(w io.Writer) { kern.box.DumpIndex(w) } type service interface { // Initialize the data for the service. Initialize() // ConfigDescriptions returns a sorted list of configuration descriptions. |
︙ | ︙ | |||
351 352 353 354 355 356 357 | Stop(*myKernel) error } type serviceConfigDescription struct{ Key, Descr string } func (kern *myKernel) SetCreators( createAuthManager kernel.CreateAuthManagerFunc, | | | | 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | Stop(*myKernel) error } type serviceConfigDescription struct{ Key, Descr string } func (kern *myKernel) SetCreators( createAuthManager kernel.CreateAuthManagerFunc, createBoxManager kernel.CreateBoxManagerFunc, setupWebServer kernel.SetupWebServerFunc, ) { kern.auth.createManager = createAuthManager kern.box.createManager = createBoxManager kern.web.setupServer = setupWebServer } |
Deleted kernel/impl/place.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to kernel/impl/web.go.
︙ | ︙ | |||
106 107 108 109 110 111 112 | func (ws *webService) Start(kern *myKernel) error { listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager) | | | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | func (ws *webService) Start(kern *myKernel) error { listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, kern.cfg.rtConfig) if err != nil { kern.doLog("Unable to create Web Server:", err) return err } if kern.debug { srvw.SetDebug() } |
︙ | ︙ |
Changes to kernel/kernel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package kernel import ( "io" "net/url" "zettelstore.de/z/auth" | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package kernel import ( "io" "net/url" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/web/server" ) // Kernel is the main internal service. type Kernel interface { // Start the service. Start(headline bool, lineServer bool) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() // SetDebug to enable/disable debug mode SetDebug(enable bool) bool |
︙ | ︙ | |||
63 64 65 66 67 68 69 | // GetServiceStatistics returns a key/value list with statistical data. GetServiceStatistics(Service) []KeyValue // DumpIndex writes some data about the internal index into a writer. DumpIndex(io.Writer) // SetCreators store functions to be called when a service has to be created. | | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | // GetServiceStatistics returns a key/value list with statistical data. GetServiceStatistics(Service) []KeyValue // DumpIndex writes some data about the internal index into a writer. DumpIndex(io.Writer) // SetCreators store functions to be called when a service has to be created. SetCreators(CreateAuthManagerFunc, CreateBoxManagerFunc, SetupWebServerFunc) } // Main references the main kernel. var Main Kernel // Unit is a type with just one value. type Unit struct{} // ShutdownChan is a channel used to signal a system shutdown. type ShutdownChan <-chan Unit // Service specifies a service, e.g. web, ... type Service uint8 // Constants for type Service. const ( _ Service = iota CoreService ConfigService AuthService BoxService WebService ) // Constants for core service system keys. const ( CoreGoArch = "go-arch" CoreGoOS = "go-os" |
︙ | ︙ | |||
106 107 108 109 110 111 112 | // Constants for authentication service keys. const ( AuthOwner = "owner" AuthReadonly = "readonly" ) | | | | | | | | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | // Constants for authentication service keys. const ( AuthOwner = "owner" AuthReadonly = "readonly" ) // Constants for box service keys. const ( BoxDefaultDirType = "defdirtype" BoxURIs = "box-uri-" ) // Allowed values for BoxDefaultDirType const ( BoxDirTypeNotify = "notify" BoxDirTypeSimple = "simple" ) // Constants for web service keys. const ( WebListenAddress = "listen" WebPersistentCookie = "persistent" WebSecureCookie = "secure" |
︙ | ︙ | |||
137 138 139 140 141 142 143 | // KeyValue is a pair of key and value. type KeyValue struct{ Key, Value string } // CreateAuthManagerFunc is called to create a new auth manager. type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error) | | | | | | | 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | // KeyValue is a pair of key and value. type KeyValue struct{ Key, Value string } // CreateAuthManagerFunc is called to create a new auth manager. type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error) // CreateBoxManagerFunc is called to create a new box manager. type CreateBoxManagerFunc func( boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config, ) (box.Manager, error) // SetupWebServerFunc is called to create a new web service handler. type SetupWebServerFunc func( webServer server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config, ) error |
Changes to parser/cleaner/cleaner.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package cleaner provides funxtions to clean up the parsed AST. package cleaner import ( "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) // CleanupBlockSlice cleans the given block slice. func CleanupBlockSlice(bs ast.BlockSlice) { | > | | > < | | < < < < | | < | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < | < > | | | | | | | | | > | | < < | < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | // Package cleaner provides funxtions to clean up the parsed AST. package cleaner import ( "strconv" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) // CleanupBlockSlice cleans the given block slice. func CleanupBlockSlice(bs ast.BlockSlice) { cv := cleanupVisitor{ textEnc: encoder.Create(api.EncoderText, nil), hasMark: false, doMark: false, } ast.WalkBlockSlice(&cv, bs) if cv.hasMark { cv.doMark = true ast.WalkBlockSlice(&cv, bs) } } type cleanupVisitor struct { textEnc encoder.Encoder ids map[string]ast.Node hasMark bool doMark bool } func (cv *cleanupVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.HeadingNode: if cv.doMark || n == nil || n.Inlines == nil { return nil } var sb strings.Builder _, err := cv.textEnc.WriteInlines(&sb, n.Inlines) if err != nil { return nil } s := strfun.Slugify(sb.String()) if len(s) > 0 { n.Slug = cv.addIdentifier(s, n) } return nil case *ast.MarkNode: if !cv.doMark { cv.hasMark = true return nil } if n.Text == "" { n.Text = cv.addIdentifier("*", n) return nil } n.Text = cv.addIdentifier(n.Text, n) return nil } return cv } 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 { |
︙ | ︙ |
Changes to parser/markdown/markdown.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 | "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" | > < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "fmt" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: "markdown", AltNames: []string{"md"}, ParseBlocks: parseBlocks, |
︙ | ︙ | |||
46 47 48 49 50 51 52 | 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)) | | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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(api.EncoderText, nil) return &mdP{source: source, docNode: node, textEnc: textEnc} } type mdP struct { source []byte docNode gmAst.Node textEnc encoder.Encoder |
︙ | ︙ | |||
121 122 123 124 125 126 127 | return &ast.HRuleNode{ Attrs: nil, //TODO } } func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode { return &ast.VerbatimNode{ | | | | 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 | return &ast.HRuleNode{ Attrs: nil, //TODO } } func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode { return &ast.VerbatimNode{ Kind: 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{ Kind: ast.VerbatimProg, Attrs: attrs, Lines: p.acceptRawText(node), } } func (p *mdP) acceptRawText(node gmAst.Node) []string { lines := node.Lines() |
︙ | ︙ | |||
159 160 161 162 163 164 165 | result = append(result, string(line)) } return result } func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *ast.NestedListNode { return &ast.NestedListNode{ | | | | | | 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 | result = append(result, string(line)) } return result } func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *ast.NestedListNode { return &ast.NestedListNode{ Kind: ast.NestedListQuote, Items: []ast.ItemSlice{ p.acceptItemSlice(node), }, } } func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode { kind := ast.NestedListUnordered var attrs *ast.Attributes if node.IsOrdered() { kind = 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{ Kind: kind, Items: items, Attrs: attrs, } } func (p *mdP) acceptItemSlice(node gmAst.Node) ast.ItemSlice { result := make(ast.ItemSlice, 0, node.ChildCount()) |
︙ | ︙ | |||
219 220 221 222 223 224 225 | 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{ | | | 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | 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{ Kind: 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() { |
︙ | ︙ | |||
288 289 290 291 292 293 294 | 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 { | | | 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | 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 input.IsSpace(ch) { if state == 1 { result = append(result, &ast.TextNode{Text: text[lastPos:pos]}) lastPos = pos } state = 2 } else { if state == 2 { |
︙ | ︙ | |||
357 358 359 360 361 362 363 | } return sb.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice { return ast.InlineSlice{ &ast.LiteralNode{ | | | 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | } return sb.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice { return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralProg, Attrs: nil, //TODO Text: cleanCodeSpan(string(node.Text(p.source))), }, } } func cleanCodeSpan(text string) string { |
︙ | ︙ | |||
387 388 389 390 391 392 393 | return text } sb.WriteString(text[lastPos:]) return sb.String() } func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice { | | | | | 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 | return text } sb.WriteString(text[lastPos:]) return sb.String() } func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice { kind := ast.FormatEmph if node.Level == 2 { kind = ast.FormatStrong } return ast.InlineSlice{ &ast.FormatNode{ Kind: kind, Attrs: nil, //TODO Inlines: p.acceptInlineSlice(node), }, } } func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice { |
︙ | ︙ | |||
478 479 480 481 482 483 484 | 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{ | | | 478 479 480 481 482 483 484 485 486 487 488 489 490 | 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{ Kind: 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 29 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package markdown provides a parser for markdown. package markdown import ( "strings" "testing" "zettelstore.de/z/ast" ) func TestSplitText(t *testing.T) { t.Parallel() var testcases = []struct { text string exp string }{ {"", ""}, {"abc", "Tabc"}, {" ", "S "}, |
︙ | ︙ |
Changes to parser/none/none.go.
︙ | ︙ | |||
35 36 37 38 39 40 41 | } return ast.BlockSlice{descrlist} } func getDescription(key, value string) ast.Description { return ast.Description{ Term: ast.InlineSlice{&ast.TextNode{Text: key}}, | | < | | | < | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | } 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.ParaNode{ Inlines: convertToInlineSlice(value, meta.Type(key)), }}, }, } } func convertToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice { var sliceData []string if dt.IsSet { |
︙ | ︙ | |||
83 84 85 86 87 88 89 | return result } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { inp.SkipToEOL() return ast.InlineSlice{ &ast.FormatNode{ | | | 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | return result } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { inp.SkipToEOL() return ast.InlineSlice{ &ast.FormatNode{ Kind: 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"}, |
︙ | ︙ |
Changes to parser/plain/plain.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" | < > > > > > > > > > > > | > > > > > > | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: "txt", AltNames: []string{"plain", "text"}, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: "html", ParseBlocks: parseBlocksHTML, ParseInlines: parseInlinesHTML, }) 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 doParseBlocks(inp, m, syntax, ast.VerbatimProg) } func parseBlocksHTML(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { return doParseBlocks(inp, m, syntax, ast.VerbatimHTML) } func doParseBlocks(inp *input.Input, m *meta.Meta, syntax string, kind ast.VerbatimKind) ast.BlockSlice { return ast.BlockSlice{ &ast.VerbatimNode{ Kind: kind, Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}}, Lines: readLines(inp), }, } } func readLines(inp *input.Input) (lines []string) { for { inp.EatEOL() posL := inp.Pos if inp.Ch == input.EOS { return lines } inp.SkipToEOL() lines = append(lines, inp.Src[posL:inp.Pos]) } } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { return doParseInlines(inp, syntax, ast.LiteralProg) } func parseInlinesHTML(inp *input.Input, syntax string) ast.InlineSlice { return doParseInlines(inp, syntax, ast.LiteralHTML) } func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice { inp.SkipToEOL() return ast.InlineSlice{ &ast.LiteralNode{ Kind: kind, Attrs: &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 { |
︙ | ︙ | |||
100 101 102 103 104 105 106 | Blob: []byte(svgSrc), Syntax: syntax, }, } } func scanSVG(inp *input.Input) string { | | | 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | Blob: []byte(svgSrc), Syntax: syntax, }, } } func scanSVG(inp *input.Input) string { for input.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.
︙ | ︙ | |||
165 166 167 168 169 170 171 | return nil, false } attrs := cp.parseAttributes(true) inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } | | | | | | | | | 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 | return nil, false } attrs := cp.parseAttributes(true) inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } var kind ast.VerbatimKind switch fch { case '`', runeModGrave: kind = ast.VerbatimProg case '%': kind = ast.VerbatimComment default: panic(fmt.Sprintf("%q is not a verbatim char", fch)) } rn = &ast.VerbatimNode{Kind: kind, 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.RegionKind{ ':': ast.RegionSpan, '<': ast.RegionQuote, '"': ast.RegionVerse, } // parseRegion parses a block region. func (cp *zmkP) parseRegion() (rn *ast.RegionNode, success bool) { inp := cp.inp fch := inp.Ch kind, ok := runeRegion[fch] if !ok { panic(fmt.Sprintf("%q is not a region char", fch)) } cnt := cp.countDelim(fch) if cnt < 3 { return nil, false } attrs := cp.parseAttributes(true) inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } rn = &ast.RegionNode{Kind: kind, Attrs: attrs} var lastPara *ast.ParaNode inp.EatEOL() for { posL := inp.Pos switch inp.Ch { case fch: if cp.countDelim(fch) >= cnt { |
︙ | ︙ | |||
303 304 305 306 307 308 309 | return nil, false } attrs := cp.parseAttributes(true) inp.SkipToEOL() return &ast.HRuleNode{Attrs: attrs}, true } | | | | | | | | > > > > | | | | | | | | | 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 | return nil, false } attrs := cp.parseAttributes(true) inp.SkipToEOL() return &ast.HRuleNode{Attrs: attrs}, true } var mapRuneNestedList = map[rune]ast.NestedListKind{ '*': ast.NestedListUnordered, '#': ast.NestedListOrdered, '>': ast.NestedListQuote, } // parseNestedList parses a list. func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) { inp := cp.inp kinds := cp.parseNestedListKinds() if kinds == nil { return nil, false } cp.skipSpace() if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) { return nil, false } if len(kinds) < len(cp.lists) { cp.lists = cp.lists[:len(kinds)] } ln, newLnCount := cp.buildNestedList(kinds) pn := cp.parseLinePara() if pn == nil { pn = &ast.ParaNode{} } ln.Items = append(ln.Items, ast.ItemSlice{pn}) return cp.cleanupParsedNestedList(newLnCount) } func (cp *zmkP) parseNestedListKinds() []ast.NestedListKind { inp := cp.inp codes := make([]ast.NestedListKind, 0, 4) for { code, ok := mapRuneNestedList[inp.Ch] if !ok { panic(fmt.Sprintf("%q is not a region char", inp.Ch)) } codes = append(codes, code) inp.Next() switch inp.Ch { case '*', '#', '>': case ' ', input.EOS, '\n', '\r': return codes default: return nil } } } func (cp *zmkP) buildNestedList(kinds []ast.NestedListKind) (ln *ast.NestedListNode, newLnCount int) { for i, kind := range kinds { if i < len(cp.lists) { if cp.lists[i].Kind != kind { ln = &ast.NestedListNode{Kind: kind} newLnCount++ cp.lists[i] = ln cp.lists = cp.lists[:i+1] } else { ln = cp.lists[i] } } else { ln = &ast.NestedListNode{Kind: kind} newLnCount++ cp.lists = append(cp.lists, ln) } } return ln, newLnCount } |
︙ | ︙ | |||
480 481 482 483 484 485 486 487 488 489 490 491 492 493 | } cp.lists = cp.lists[:cnt] if cnt == 0 { return false } ln := cp.lists[cnt-1] pn := cp.parseLinePara() lbn := ln.Items[len(ln.Items)-1] if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok { lpn.Inlines = append(lpn.Inlines, pn.Inlines...) } else { ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn) } return true | > > > | 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 | } cp.lists = cp.lists[:cnt] if cnt == 0 { return false } ln := cp.lists[cnt-1] pn := cp.parseLinePara() if pn == nil { pn = &ast.ParaNode{} } lbn := ln.Items[len(ln.Items)-1] if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok { lpn.Inlines = append(lpn.Inlines, pn.Inlines...) } else { ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn) } return true |
︙ | ︙ |
Changes to parser/zettelmark/inline.go.
︙ | ︙ | |||
185 186 187 188 189 190 191 | if !ok { return "", nil, false } if inp.Ch == '|' { // First part must be inline text if pos == inp.Pos { // [[| or {{| return "", nil, false } | | < | > > > > | > < > > < | 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 | if !ok { return "", nil, false } if inp.Ch == '|' { // First part must be inline text if pos == inp.Pos { // [[| or {{| return "", nil, false } cp.inp = input.NewInput(inp.Src[pos:inp.Pos]) for { in := cp.parseInline() if in == nil { break } ins = append(ins, in) } cp.inp = inp inp.Next() } else if hasSpace { return "", nil, false } else { inp.SetPos(pos) } cp.skipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } ref = inp.Src[pos:inp.Pos] inp.Next() |
︙ | ︙ | |||
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 | switch inp.Ch { case input.EOS: return false, false case '\n', '\r', ' ': hasSpace = true case '|': return hasSpace, true case closeCh: inp.Next() if inp.Ch == closeCh { return hasSpace, true } continue } inp.Next() } } func (cp *zmkP) readReferenceToClose(closeCh rune) bool { inp := cp.inp for { switch inp.Ch { case input.EOS, '\n', '\r', ' ': return false case closeCh: return true } inp.Next() } } | > > > > > > > > > > > > > > > > > > > > | 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 | switch inp.Ch { case input.EOS: return false, false case '\n', '\r', ' ': hasSpace = true case '|': return hasSpace, true case '\\': inp.Next() switch inp.Ch { case input.EOS: return false, false case '\n', '\r': hasSpace = true } case '%': inp.Next() if inp.Ch == '%' { inp.SkipToEOL() } continue case closeCh: inp.Next() if inp.Ch == closeCh { return hasSpace, true } continue } inp.Next() } } func (cp *zmkP) readReferenceToClose(closeCh rune) bool { inp := cp.inp for { switch inp.Ch { case input.EOS, '\n', '\r', ' ': return false case '\\': inp.Next() switch inp.Ch { case input.EOS, '\n', '\r': return false } case closeCh: return true } inp.Next() } } |
︙ | ︙ | |||
358 359 360 361 362 363 364 | for inp.Ch == '%' { inp.Next() } cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { | | | | | | 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 | for inp.Ch == '%' { inp.Next() } cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return &ast.LiteralNode{Kind: ast.LiteralComment, Text: inp.Src[pos:inp.Pos]}, true } inp.Next() } } var mapRuneFormat = map[rune]ast.FormatKind{ '/': 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 kind, ok := mapRuneFormat[fch] if !ok { panic(fmt.Sprintf("%q is not a formatting char", fch)) } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } inp.Next() fn := &ast.FormatNode{Kind: kind} for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { inp.Next() if inp.Ch == fch { |
︙ | ︙ | |||
412 413 414 415 416 417 418 | return nil, false } fn.Inlines = append(fn.Inlines, in) } } } | | | | | 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 | return nil, false } fn.Inlines = append(fn.Inlines, in) } } } var mapRuneLiteral = map[rune]ast.LiteralKind{ '`': ast.LiteralProg, runeModGrave: ast.LiteralProg, '+': ast.LiteralKeyb, '=': ast.LiteralOutput, } func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) { inp := cp.inp fch := inp.Ch kind, ok := mapRuneLiteral[fch] if !ok { panic(fmt.Sprintf("%q is not a formatting char", fch)) } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } fn := &ast.LiteralNode{Kind: kind} inp.Next() var sb strings.Builder for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { |
︙ | ︙ |
Changes to parser/zettelmark/node.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark | < | < < < < < < < | 1 2 3 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 zettelmark provides a parser for zettelmarkup. package zettelmark import "zettelstore.de/z/ast" // Internal nodes for parsing zettelmark. These will be removed in // post-processing. // nullItemNode specifies a removable placeholder for an item node. type nullItemNode struct { ast.ItemNode } // nullDescriptionNode specifies a removable placeholder. type nullDescriptionNode struct { ast.DescriptionNode } |
Changes to parser/zettelmark/post-processor.go.
︙ | ︙ | |||
30 31 32 33 34 35 36 | } // postProcessor is a visitor that cleans the abstract syntax tree. type postProcessor struct { inVerse bool } | < | | > | < < < < < < < | | | | | | | | < < < | | < < < < < < | | | | < < < | | | | | | | < < < | | | | | | | | | | | | | | | | | | > > > > > > > > > > > > > > > > > > | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | } // postProcessor is a visitor that cleans the abstract syntax tree. type postProcessor struct { inVerse bool } func (pp *postProcessor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: n.Inlines = pp.processInlineSlice(n.Inlines) case *ast.RegionNode: oldVerse := pp.inVerse if n.Kind == ast.RegionVerse { pp.inVerse = true } n.Blocks = pp.processBlockSlice(n.Blocks) pp.inVerse = oldVerse n.Inlines = pp.processInlineSlice(n.Inlines) case *ast.HeadingNode: n.Inlines = pp.processInlineSlice(n.Inlines) case *ast.NestedListNode: for i, item := range n.Items { n.Items[i] = pp.processItemSlice(item) } case *ast.DescriptionListNode: for i, def := range n.Descriptions { n.Descriptions[i].Term = pp.processInlineSlice(def.Term) for j, b := range def.Descriptions { n.Descriptions[i].Descriptions[j] = pp.processDescriptionSlice(b) } } case *ast.TableNode: width := tableWidth(n) n.Align = make([]ast.Alignment, width) for i := 0; i < width; i++ { n.Align[i] = ast.AlignDefault } if len(n.Rows) > 0 && isHeaderRow(n.Rows[0]) { n.Header = n.Rows[0] n.Rows = n.Rows[1:] pp.visitTableHeader(n) } if len(n.Header) > 0 { n.Header = appendCells(n.Header, width, n.Align) for i, cell := range n.Header { pp.processCell(cell, n.Align[i]) } } pp.visitTableRows(n, width) case *ast.LinkNode: n.Inlines = pp.processInlineSlice(n.Inlines) case *ast.ImageNode: n.Inlines = pp.processInlineSlice(n.Inlines) case *ast.CiteNode: n.Inlines = pp.processInlineSlice(n.Inlines) case *ast.FootnoteNode: n.Inlines = pp.processInlineSlice(n.Inlines) case *ast.FormatNode: if n.Attrs != nil && n.Attrs.HasDefault() { if newKind, ok := mapSemantic[n.Kind]; ok { n.Attrs.RemoveDefault() n.Kind = newKind } } n.Inlines = pp.processInlineSlice(n.Inlines) } return nil } func (pp *postProcessor) visitTableHeader(tn *ast.TableNode) { for pos, cell := range tn.Header { inlines := cell.Inlines if len(inlines) == 0 { continue |
︙ | ︙ | |||
190 191 192 193 194 195 196 | } } else { cell.Align = colAlign } cell.Inlines = pp.processInlineSlice(cell.Inlines) } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < | | > | 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 | } } else { cell.Align = colAlign } cell.Inlines = pp.processInlineSlice(cell.Inlines) } var mapSemantic = map[ast.FormatKind]ast.FormatKind{ ast.FormatItalic: ast.FormatEmph, ast.FormatBold: ast.FormatStrong, ast.FormatUnder: ast.FormatInsert, ast.FormatStrike: ast.FormatDelete, } // 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 { if len(bns) == 0 { return nil } ast.WalkBlockSlice(pp, bns) 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 { |
︙ | ︙ | |||
281 282 283 284 285 286 287 288 | } 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 { | > > > | | 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | } 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 { if len(ins) == 0 { return nil } for _, in := range ins { ast.Walk(pp, in) } fromPos, toPos := 0, 0 for fromPos < len(ins) { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.ParaNode: |
︙ | ︙ | |||
308 309 310 311 312 313 314 315 | } 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 { | > > > | | 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | } return ins[:toPos:toPos] } // processDescriptionSlice post-processes a slice of descriptions. // It is one of the working horses for post-processing. func (pp *postProcessor) processDescriptionSlice(dns ast.DescriptionSlice) ast.DescriptionSlice { if len(dns) == 0 { return nil } for _, dn := range dns { ast.Walk(pp, dn) } fromPos, toPos := 0, 0 for fromPos < len(dns) { dns[toPos] = dns[fromPos] fromPos++ switch dn := dns[toPos].(type) { case *ast.ParaNode: |
︙ | ︙ | |||
337 338 339 340 341 342 343 | // 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 } | | < < | 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | // 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 } ast.WalkInlineSlice(pp, ins) if !pp.inVerse { ins = processInlineSliceHead(ins) } toPos := pp.processInlineSliceCopy(ins) toPos = pp.processInlineSliceTail(ins, toPos) ins = ins[:toPos:toPos] |
︙ | ︙ |
Changes to parser/zettelmark/zettelmark_test.go.
︙ | ︙ | |||
46 47 48 49 50 51 52 | 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 | | > > | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 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 ast.WalkBlockSlice(&tv, bns) got := tv.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) } }) } } func TestEOL(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"", ""}, {"\n", ""}, {"\r", ""}, {"\r\n", ""}, {"\n\n", ""}, }) } func TestText(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abcd", "(PARA abcd)"}, {"ab cd", "(PARA ab SP cd)"}, {"abcd ", "(PARA abcd)"}, {" abcd", "(PARA abcd)"}, {"\\", "(PARA \\)"}, {"\\\n", ""}, |
︙ | ︙ | |||
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | {"...?", "(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 [[]])"}, | > > > > | 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | {"...?", "(PARA \u2026?)"}, {"...-", "(PARA ...-)"}, {"a...b", "(PARA a...b)"}, }) } func TestSpace(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {" ", ""}, {"\t", ""}, {" ", ""}, }) } func TestSoftBreak(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"x\ny", "(PARA x SB y)"}, {"z\n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestHardBreak(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"x \ny", "(PARA x HB y)"}, {"z \n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestLink(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[", "(PARA [)"}, {"[[", "(PARA [[)"}, {"[[|", "(PARA [[|)"}, {"[[]", "(PARA [[])"}, {"[[|]", "(PARA [[|])"}, {"[[]]", "(PARA [[]])"}, |
︙ | ︙ | |||
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 | {"[[a]]go", "(PARA (LINK a a) go)"}, {"[[a]]{go}", "(PARA (LINK a a)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA (LINK [[a [[a) |b]])"}, {"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"}, {"[[[b]c|d]]", "(PARA (LINK d [b]c))"}, {"[[a[]c|d]]", "(PARA (LINK d a[]c))"}, {"[[a[b]|d]]", "(PARA (LINK d a[b]))"}, }) } func TestCite(t *testing.T) { checkTcs(t, TestCases{ {"[@", "(PARA [@)"}, {"[@]", "(PARA [@])"}, {"[@a]", "(PARA (CITE a))"}, {"[@ a]", "(PARA [@ SP a])"}, {"[@a ]", "(PARA (CITE a))"}, {"[@a\n]", "(PARA (CITE a))"}, {"[@a\nx]", "(PARA (CITE a SB x))"}, {"[@a\n\n]", "(PARA [@a)(PARA ])"}, {"[@a,\n]", "(PARA (CITE a))"}, {"[@a,n]", "(PARA (CITE a n))"}, {"[@a| n]", "(PARA (CITE a n))"}, {"[@a|n ]", "(PARA (CITE a n))"}, {"[@a,[@b]]", "(PARA (CITE a (CITE b)))"}, {"[@a]{color=green}", "(PARA (CITE a)[ATTR color=green])"}, }) } func TestFootnote(t *testing.T) { checkTcs(t, TestCases{ {"[^", "(PARA [^)"}, {"[^]", "(PARA (FN))"}, {"[^abc]", "(PARA (FN abc))"}, {"[^abc ]", "(PARA (FN abc))"}, {"[^abc\ndef]", "(PARA (FN abc SB def))"}, {"[^abc\n\ndef]", "(PARA [^abc)(PARA def])"}, {"[^abc[^def]]", "(PARA (FN abc (FN def)))"}, {"[^abc]{-}", "(PARA (FN abc)[ATTR -])"}, }) } func TestImage(t *testing.T) { checkTcs(t, TestCases{ {"{", "(PARA {)"}, {"{{", "(PARA {{)"}, {"{{|", "(PARA {{|)"}, {"{{}", "(PARA {{})"}, {"{{|}", "(PARA {{|})"}, {"{{}}", "(PARA {{}})"}, | > > > > > > > > > > > > | 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 | {"[[a]]go", "(PARA (LINK a a) go)"}, {"[[a]]{go}", "(PARA (LINK a a)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA (LINK [[a [[a) |b]])"}, {"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"}, {"[[[b]c|d]]", "(PARA (LINK d [b]c))"}, {"[[a[]c|d]]", "(PARA (LINK d a[]c))"}, {"[[a[b]|d]]", "(PARA (LINK d a[b]))"}, {"[[\\|]]", "(PARA (LINK %5C%7C \\|))"}, {"[[\\||a]]", "(PARA (LINK a |))"}, {"[[b\\||a]]", "(PARA (LINK a b|))"}, {"[[b\\|c|a]]", "(PARA (LINK a b|c))"}, {"[[\\]]]", "(PARA (LINK %5C%5D \\]))"}, {"[[\\]|a]]", "(PARA (LINK a ]))"}, {"[[b\\]|a]]", "(PARA (LINK a b]))"}, {"[[\\]\\||a]]", "(PARA (LINK a ]|))"}, {"[[http://a|http://a]]", "(PARA (LINK http://a http://a))"}, }) } func TestCite(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[@", "(PARA [@)"}, {"[@]", "(PARA [@])"}, {"[@a]", "(PARA (CITE a))"}, {"[@ a]", "(PARA [@ SP a])"}, {"[@a ]", "(PARA (CITE a))"}, {"[@a\n]", "(PARA (CITE a))"}, {"[@a\nx]", "(PARA (CITE a SB x))"}, {"[@a\n\n]", "(PARA [@a)(PARA ])"}, {"[@a,\n]", "(PARA (CITE a))"}, {"[@a,n]", "(PARA (CITE a n))"}, {"[@a| n]", "(PARA (CITE a n))"}, {"[@a|n ]", "(PARA (CITE a n))"}, {"[@a,[@b]]", "(PARA (CITE a (CITE b)))"}, {"[@a]{color=green}", "(PARA (CITE a)[ATTR color=green])"}, }) } func TestFootnote(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[^", "(PARA [^)"}, {"[^]", "(PARA (FN))"}, {"[^abc]", "(PARA (FN abc))"}, {"[^abc ]", "(PARA (FN abc))"}, {"[^abc\ndef]", "(PARA (FN abc SB def))"}, {"[^abc\n\ndef]", "(PARA [^abc)(PARA def])"}, {"[^abc[^def]]", "(PARA (FN abc (FN def)))"}, {"[^abc]{-}", "(PARA (FN abc)[ATTR -])"}, }) } func TestImage(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"{", "(PARA {)"}, {"{{", "(PARA {{)"}, {"{{|", "(PARA {{|)"}, {"{{}", "(PARA {{})"}, {"{{|}", "(PARA {{|})"}, {"{{}}", "(PARA {{}})"}, |
︙ | ︙ | |||
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 | {"{{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})"}, | > > > > > > > > > > > > > | 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 | {"{{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}})"}, {"{{\\|}}", "(PARA (IMAGE %5C%7C))"}, {"{{\\||a}}", "(PARA (IMAGE a |))"}, {"{{b\\||a}}", "(PARA (IMAGE a b|))"}, {"{{b\\|c|a}}", "(PARA (IMAGE a b|c))"}, {"{{\\}}}", "(PARA (IMAGE %5C%7D))"}, {"{{\\}|a}}", "(PARA (IMAGE a }))"}, {"{{b\\}|a}}", "(PARA (IMAGE a b}))"}, {"{{\\}\\||a}}", "(PARA (IMAGE a }|))"}, {"{{http://a|http://a}}", "(PARA (IMAGE http://a http://a))"}, }) } func TestTag(t *testing.T) { t.Parallel() 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) { t.Parallel() 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) { t.Parallel() checkTcs(t, TestCases{ {"%", "(PARA %)"}, {"%%", "(PARA {%})"}, {"%\n", "(PARA %)"}, {"%%\n", "(PARA {%})"}, {"%%a", "(PARA {% a})"}, {"%%%a", "(PARA {% a})"}, {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"a%%b", "(PARA a {% b})"}, {"a %%b", "(PARA a SP {% b})"}, {" %%b", "(PARA {% b})"}, {"%%b ", "(PARA {% b })"}, {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { t.Parallel() for _, ch := range []string{"/", "*", "_", "~", "'", "^", ",", "<", "\"", ";", ":"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, |
︙ | ︙ | |||
294 295 296 297 298 299 300 301 302 303 304 305 306 307 | {"//****//", "(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})"}, | > | 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | {"//****//", "(PARA {/ {*}})"}, {"//**a**//", "(PARA {/ {* a}})"}, {"//**//**", "(PARA // {* //})"}, }) } func TestLiteral(t *testing.T) { t.Parallel() for _, ch := range []string{"`", "+", "="} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, |
︙ | ︙ | |||
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 | {"++``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)"}, | > > > > > > > > | 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 | {"++``a``++", "(PARA {+ ``a``})"}, {"++``++``", "(PARA {+ ``} ``)"}, {"++\\+++", "(PARA {+ +})"}, }) } func TestMixFormatCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"//abc//\n**def**", "(PARA {/ abc} SB {* def})"}, {"++abc++\n==def==", "(PARA {+ abc} SB {= def})"}, {"//abc//\n==def==", "(PARA {/ abc} SB {= def})"}, {"//abc//\n``def``", "(PARA {/ abc} SB {` def})"}, {"\"\"ghi\"\"\n::abc::\n``def``\n", "(PARA {\" ghi} SB {: abc} SB {` def})"}, }) } func TestNDash(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"--", "(PARA \u2013)"}, {"a--b", "(PARA a\u2013b)"}, }) } func TestEntity(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"&", "(PARA &)"}, {"&;", "(PARA &;)"}, {"&#;", "(PARA &#;)"}, {"a;", "(PARA & #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) { t.Parallel() checkTcs(t, TestCases{ {"```\n```", "(PROG)"}, {"```\nabc\n```", "(PROG\nabc)"}, {"```\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n```\n````", "(PROG\nabc\n```)"}, {"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"}, }) } func TestSpanRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {":::\n:::", "(SPAN)"}, {":::\nabc\n:::", "(SPAN (PARA abc))"}, {":::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n:::\ndef\n:::\n::::", "(SPAN (PARA abc)(SPAN (PARA def)))"}, {":::{go}\n:::", "(SPAN)[ATTR go]"}, {":::\nabc\n::: def ", "(SPAN (PARA abc) (LINE def))"}, }) } func TestQuoteRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"<<<\n<<<", "(QUOTE)"}, {"<<<\nabc\n<<<", "(QUOTE (PARA abc))"}, {"<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(QUOTE (PARA abc)(QUOTE (PARA def)))"}, {"<<<go\n<<<", "(QUOTE)[ATTR =go]"}, {"<<<\nabc\n<<< def ", "(QUOTE (PARA abc) (LINE def))"}, }) } func TestVerseRegion(t *testing.T) { t.Parallel() checkTcs(t, replace("\"", TestCases{ {"$$$\n$$$", "(VERSE)"}, {"$$$\nabc\n$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\ndef\n$$$", "(VERSE (PARA abc HB def))"}, {"$$$$\nabc\n$$$\ndef\n$$$\n$$$$", "(VERSE (PARA abc)(VERSE (PARA def)))"}, {"$$$go\n$$$", "(VERSE)[ATTR =go]"}, {"$$$\nabc\n$$$ def ", "(VERSE (PARA abc) (LINE def))"}, })) } func TestHeading(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"=h", "(PARA =h)"}, {"= h", "(PARA = SP h)"}, {"==h", "(PARA ==h)"}, {"== h", "(PARA == SP h)"}, {"===h", "(PARA ===h)"}, {"=== h", "(H2 h)"}, |
︙ | ︙ | |||
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 | {" =", "(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 $)"}, | > > | 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 | {" =", "(PARA =)"}, {"=== h\na", "(H2 h)(PARA a)"}, {"=== h i {-}", "(H2 h SP i)[ATTR -]"}, }) } func TestHRule(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"-", "(PARA -)"}, {"---", "(HR)"}, {"----", "(HR)"}, {"---A", "(HR)[ATTR =A]"}, {"---A-", "(HR)[ATTR =A-]"}, {"-1", "(PARA -1)"}, {"2-1", "(PARA 2-1)"}, {"--- { go } ", "(HR)[ATTR go]"}, {"--- { .go } ", "(HR)[ATTR class=go]"}, }) } func TestList(t *testing.T) { t.Parallel() // No ">" in the following, because quotation lists may have empty items. for _, ch := range []string{"*", "#"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$ ", "(PARA $)"}, |
︙ | ︙ | |||
484 485 486 487 488 489 490 | // 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)})"}, | | > > | 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 | // A HRule creates a new list {"* abc\n---\n* def", "(UL {(PARA abc)})(HR)(UL {(PARA def)})"}, // Changing list type adds a new list {"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"}, // Quotation lists may have empty items {">", "(QL {})"}, }) } func TestEnumAfterPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abc\n* def", "(PARA abc)(UL {(PARA def)})"}, {"abc\n*def", "(PARA abc SB *def)"}, }) } func TestDefinition(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {";", "(PARA ;)"}, {"; ", "(PARA ;)"}, {"; abc", "(DL (DT abc))"}, {"; abc\ndef", "(DL (DT abc))(PARA def)"}, {"; abc\n def", "(DL (DT abc))(PARA def)"}, {"; abc\n def", "(DL (DT abc SB def))"}, |
︙ | ︙ | |||
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 | {"; abc\n:", "(DL (DT abc))(PARA :)"}, {"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"}, {"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"}, }) } func TestTable(t *testing.T) { checkTcs(t, TestCases{ {"|", "(TAB (TR))"}, {"|a", "(TAB (TR (TD a)))"}, {"|a|", "(TAB (TR (TD a)))"}, {"|a| ", "(TAB (TR (TD a)(TD)))"}, {"|a|b", "(TAB (TR (TD a)(TD b)))"}, {"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, {"|%", ""}, {"|a|b\n|%---\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, }) } func TestBlockAttr(t *testing.T) { checkTcs(t, TestCases{ {":::go\n:::", "(SPAN)[ATTR =go]"}, {":::go=\n:::", "(SPAN)[ATTR =go]"}, {":::{}\n:::", "(SPAN)"}, {":::{ }\n:::", "(SPAN)"}, {":::{.go}\n:::", "(SPAN)[ATTR class=go]"}, {":::{=go}\n:::", "(SPAN)[ATTR =go]"}, | > > | 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 | {"; abc\n:", "(DL (DT abc))(PARA :)"}, {"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"}, {"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"}, }) } func TestTable(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"|", "(TAB (TR))"}, {"|a", "(TAB (TR (TD a)))"}, {"|a|", "(TAB (TR (TD a)))"}, {"|a| ", "(TAB (TR (TD a)(TD)))"}, {"|a|b", "(TAB (TR (TD a)(TD b)))"}, {"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, {"|%", ""}, {"|a|b\n|%---\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, }) } func TestBlockAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {":::go\n:::", "(SPAN)[ATTR =go]"}, {":::go=\n:::", "(SPAN)[ATTR =go]"}, {":::{}\n:::", "(SPAN)"}, {":::{ }\n:::", "(SPAN)"}, {":::{.go}\n:::", "(SPAN)[ATTR class=go]"}, {":::{=go}\n:::", "(SPAN)[ATTR =go]"}, |
︙ | ︙ | |||
567 568 569 570 571 572 573 574 575 576 577 578 579 580 | {":::{.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])"}, | > | 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 | {":::{.go .py}\n:::", "(SPAN)[ATTR class=$go py$]"}, {":::{go go}\n:::", "(SPAN)[ATTR go]"}, {":::{=py =go}\n:::", "(SPAN)[ATTR =go]"}, })) } func TestInlineAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"::a::{}", "(PARA {: a})"}, {"::a::{ }", "(PARA {: a})"}, {"::a::{.go}", "(PARA {: a}[ATTR class=go])"}, {"::a::{=go}", "(PARA {: a}[ATTR =go])"}, {"::a::{go}", "(PARA {: a}[ATTR go])"}, {"::a::{go=py}", "(PARA {: a}[ATTR go=py])"}, |
︙ | ︙ | |||
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 | {"::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() } | > > | > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > | > > > > > > > > > > > > | < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | > | 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 | {"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"}, })) } func TestTemp(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"", ""}, }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { b strings.Builder } func (tv *TestVisitor) String() string { return tv.b.String() } func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: tv.b.WriteString("(PARA") tv.visitInlineSlice(n.Inlines) tv.b.WriteByte(')') case *ast.VerbatimNode: code, ok := mapVerbatimKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind)) } tv.b.WriteString(code) for _, line := range n.Lines { tv.b.WriteByte('\n') tv.b.WriteString(line) } tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.RegionNode: code, ok := mapRegionKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown region code %v", n.Kind)) } tv.b.WriteString(code) if n.Blocks != nil { tv.b.WriteByte(' ') ast.WalkBlockSlice(tv, n.Blocks) } if len(n.Inlines) > 0 { tv.b.WriteString(" (LINE") tv.visitInlineSlice(n.Inlines) tv.b.WriteByte(')') } tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HeadingNode: fmt.Fprintf(&tv.b, "(H%d", n.Level) tv.visitInlineSlice(n.Inlines) tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HRuleNode: tv.b.WriteString("(HR)") tv.visitAttributes(n.Attrs) case *ast.NestedListNode: tv.b.WriteString(mapNestedListKind[n.Kind]) for _, item := range n.Items { tv.b.WriteString(" {") ast.WalkItemSlice(tv, item) tv.b.WriteByte('}') } tv.b.WriteByte(')') case *ast.DescriptionListNode: tv.b.WriteString("(DL") for _, def := range n.Descriptions { tv.b.WriteString(" (DT") tv.visitInlineSlice(def.Term) tv.b.WriteByte(')') for _, b := range def.Descriptions { tv.b.WriteString(" (DD ") ast.WalkDescriptionSlice(tv, b) tv.b.WriteByte(')') } } tv.b.WriteByte(')') case *ast.TableNode: tv.b.WriteString("(TAB") if len(n.Header) > 0 { tv.b.WriteString(" (TR") for _, cell := range n.Header { tv.b.WriteString(" (TH") tv.b.WriteString(alignString[cell.Align]) tv.visitInlineSlice(cell.Inlines) tv.b.WriteString(")") } tv.b.WriteString(")") } if len(n.Rows) > 0 { tv.b.WriteString(" ") for _, row := range n.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(")") case *ast.BLOBNode: tv.b.WriteString("(BLOB ") tv.b.WriteString(n.Syntax) tv.b.WriteString(")") case *ast.TextNode: tv.b.WriteString(n.Text) case *ast.TagNode: tv.b.WriteByte('#') tv.b.WriteString(n.Tag) tv.b.WriteByte('#') case *ast.SpaceNode: if len(n.Lexeme) == 1 { tv.b.WriteString("SP") } else { fmt.Fprintf(&tv.b, "SP%d", len(n.Lexeme)) } case *ast.BreakNode: if n.Hard { tv.b.WriteString("HB") } else { tv.b.WriteString("SB") } case *ast.LinkNode: fmt.Fprintf(&tv.b, "(LINK %v", n.Ref) tv.visitInlineSlice(n.Inlines) tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.ImageNode: fmt.Fprintf(&tv.b, "(IMAGE %v", n.Ref) tv.visitInlineSlice(n.Inlines) tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.CiteNode: fmt.Fprintf(&tv.b, "(CITE %s", n.Key) tv.visitInlineSlice(n.Inlines) tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.FootnoteNode: tv.b.WriteString("(FN") tv.visitInlineSlice(n.Inlines) tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.MarkNode: tv.b.WriteString("(MARK") if len(n.Text) > 0 { tv.b.WriteByte(' ') tv.b.WriteString(n.Text) } tv.b.WriteByte(')') case *ast.FormatNode: fmt.Fprintf(&tv.b, "{%c", mapFormatKind[n.Kind]) tv.visitInlineSlice(n.Inlines) tv.b.WriteByte('}') tv.visitAttributes(n.Attrs) case *ast.LiteralNode: code, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("No element for code %v", n.Kind)) } tv.b.WriteByte('{') tv.b.WriteRune(code) if len(n.Text) > 0 { tv.b.WriteByte(' ') tv.b.WriteString(n.Text) } tv.b.WriteByte('}') tv.visitAttributes(n.Attrs) } return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimProg: "(PROG", } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "(SPAN", ast.RegionQuote: "(QUOTE", ast.RegionVerse: "(VERSE", } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "(OL", ast.NestedListUnordered: "(UL", ast.NestedListQuote: "(QL", } var alignString = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "l", ast.AlignCenter: "c", ast.AlignRight: "r", } var mapFormatKind = map[ast.FormatKind]rune{ ast.FormatItalic: '/', ast.FormatBold: '*', ast.FormatUnder: '_', ast.FormatStrike: '~', ast.FormatMonospace: '\'', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatQuotation: '<', ast.FormatSmall: ';', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralProg: '`', ast.LiteralKeyb: '+', ast.LiteralOutput: '=', ast.LiteralComment: '%', } func (tv *TestVisitor) visitInlineSlice(ins ast.InlineSlice) { for _, in := range ins { tv.b.WriteByte(' ') ast.Walk(tv, in) } } 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)) |
︙ | ︙ |
Deleted place/constplace/base.css.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/base.mustache.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/constplace.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/context.mustache.
|
| < < < < < < < < < < < < < < < < |
Deleted place/constplace/contributors.zettel.
|
| < < < < < < < < |
Deleted place/constplace/delete.mustache.
|
| < < < < < < < < < < < < < < < |
Deleted place/constplace/dependencies.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/emoji_spin.gif.
cannot compute difference between binary files
Deleted place/constplace/error.mustache.
|
| < < < < < < |
Deleted place/constplace/form.mustache.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/home.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/info.mustache.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/license.txt.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/listroles.mustache.
|
| < < < < < < < < |
Deleted place/constplace/listtags.mustache.
|
| < < < < < < < < < < |
Deleted place/constplace/listzettel.mustache.
|
| < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/login.mustache.
|
| < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/newtoc.zettel.
|
| < < < < |
Deleted place/constplace/rename.mustache.
|
| < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/zettel.mustache.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/directory/directory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/dirplace.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/makedir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/notifydir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/service.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/watch.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/watch_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/service.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/simpledir/simpledir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/fileplace/fileplace.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/fileplace/zipplace.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/anteroom.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/anteroom_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/collect.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/enrich.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/indexer.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/memstore/memstore.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/memstore/refs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/memstore/refs_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/place.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/store.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/wordset.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/wordset_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/memplace/memplace.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/merge.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/place.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/progplace/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/progplace/keys.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/progplace/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/progplace/progplace.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/progplace/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted runes/runes.go.
|
| < < < < < < < < < < < < < < < < < < < < < |
Added search/compile.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package search provides a zettel search. package search // This file is about "compiling" a search expression into a function. import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" ) func compileFullSearch(searcher Searcher, search []expValue) MetaMatchFunc { normSearch := compileNormalizedSearch(searcher, search) plainSearch := compilePlainSearch(searcher, search) if normSearch == nil { if plainSearch == nil { return nil } return plainSearch } if plainSearch == nil { return normSearch } return func(m *meta.Meta) bool { return normSearch(m) || plainSearch(m) } } func compileNormalizedSearch(searcher Searcher, search []expValue) MetaMatchFunc { var positives, negatives []expValue posSet := make(map[string]bool) negSet := make(map[string]bool) for _, val := range search { for _, word := range strfun.NormalizeWords(val.value) { if val.negate { if _, ok := negSet[word]; !ok { negSet[word] = true negatives = append(negatives, expValue{ value: word, op: val.op, negate: true, }) } } else { if _, ok := posSet[word]; !ok { posSet[word] = true positives = append(positives, expValue{ value: word, op: val.op, negate: false, }) } } } } return compileSearch(searcher, positives, negatives) } func compilePlainSearch(searcher Searcher, search []expValue) MetaMatchFunc { var positives, negatives []expValue for _, val := range search { if val.negate { negatives = append(negatives, expValue{ value: strings.ToLower(strings.TrimSpace(val.value)), op: val.op, negate: true, }) } else { positives = append(positives, expValue{ value: strings.ToLower(strings.TrimSpace(val.value)), op: val.op, negate: false, }) } } return compileSearch(searcher, positives, negatives) } func compileSearch(searcher Searcher, poss, negs []expValue) MetaMatchFunc { if len(poss) == 0 { if len(negs) == 0 { return nil } return makeNegOnlySearch(searcher, negs) } if len(negs) == 0 { return makePosOnlySearch(searcher, poss) } return makePosNegSearch(searcher, poss, negs) } func makePosOnlySearch(searcher Searcher, poss []expValue) MetaMatchFunc { retrievePos := compileRetrieveZids(searcher, poss) var ids id.Set return func(m *meta.Meta) bool { if ids == nil { ids = retrievePos() } _, ok := ids[m.Zid] return ok } } func makeNegOnlySearch(searcher Searcher, negs []expValue) MetaMatchFunc { retrieveNeg := compileRetrieveZids(searcher, negs) var ids id.Set return func(m *meta.Meta) bool { if ids == nil { ids = retrieveNeg() } _, ok := ids[m.Zid] return !ok } } func makePosNegSearch(searcher Searcher, poss, negs []expValue) MetaMatchFunc { retrievePos := compileRetrieveZids(searcher, poss) retrieveNeg := compileRetrieveZids(searcher, negs) var ids id.Set return func(m *meta.Meta) bool { if ids == nil { ids = retrievePos() ids.Remove(retrieveNeg()) } _, okPos := ids[m.Zid] return okPos } } func compileRetrieveZids(searcher Searcher, values []expValue) func() id.Set { selFuncs := make([]selectorFunc, 0, len(values)) stringVals := make([]string, 0, len(values)) for _, val := range values { selFuncs = append(selFuncs, compileSelectOp(searcher, val.op)) stringVals = append(stringVals, val.value) } if len(selFuncs) == 0 { return func() id.Set { return id.NewSet() } } if len(selFuncs) == 1 { return func() id.Set { return selFuncs[0](stringVals[0]) } } return func() id.Set { result := selFuncs[0](stringVals[0]) for i, f := range selFuncs[1:] { result = result.Intersect(f(stringVals[i+1])) } return result } } type selectorFunc func(string) id.Set func compileSelectOp(searcher Searcher, op compareOp) selectorFunc { switch op { case cmpDefault, cmpContains: return searcher.SearchContains case cmpEqual: return searcher.SearchEqual case cmpPrefix: return searcher.SearchPrefix case cmpSuffix: return searcher.SearchSuffix default: panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) } } |
Deleted search/filter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to search/print.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "io" "sort" "strconv" "zettelstore.de/z/domain/meta" ) | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | "io" "sort" "strconv" "zettelstore.de/z/domain/meta" ) // Print the search to a writer. func (s *Search) Print(w io.Writer) { if s.negate { io.WriteString(w, "NOT (") } space := false if len(s.search) > 0 { io.WriteString(w, "ANY") printSelectExprValues(w, s.search) space = true } names := make([]string, 0, len(s.tags)) for name := range s.tags { names = append(names, name) } sort.Strings(names) for _, name := range names { if space { io.WriteString(w, " AND ") } io.WriteString(w, name) printSelectExprValues(w, s.tags[name]) space = true } if s.negate { io.WriteString(w, ")") space = true } |
︙ | ︙ | |||
72 73 74 75 76 77 78 | if lim := s.limit; lim > 0 { _ = printSpace(w, space) io.WriteString(w, "LIMIT ") io.WriteString(w, strconv.Itoa(lim)) } } | | | 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | if lim := s.limit; lim > 0 { _ = printSpace(w, space) io.WriteString(w, "LIMIT ") io.WriteString(w, strconv.Itoa(lim)) } } func printSelectExprValues(w io.Writer, values []expValue) { if len(values) == 0 { io.WriteString(w, " MATCH ANY") return } for j, val := range values { if j > 0 { |
︙ | ︙ |
Changes to search/search.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) | | | | | | | | | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Searcher is used to select zettel identifier based on search criteria. type Searcher interface { // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. SearchEqual(word string) id.Set // Select all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. SearchPrefix(prefix string) id.Set // Select all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. SearchSuffix(suffix string) id.Set // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. SearchContains(s string) id.Set } // MetaMatchFunc is a function determine whethe some metadata should be selected or not. type MetaMatchFunc func(*meta.Meta) bool // Search specifies a mechanism for selecting zettel. type Search struct { mx sync.RWMutex // Protects other attributes // Fields to be used for selecting preMatch MetaMatchFunc // Match that must be true tags expTagValues // Expected values for a tag search []expValue // Search string negate bool // Negate the result of the whole selecting process // Fields to be used for sorting order string // Name of meta key. None given: use "id" descending bool // Sort by order, but descending offset int // <= 0: no offset limit int // <= 0: no limit } |
︙ | ︙ | |||
77 78 79 80 81 82 83 | type expValue struct { value string op compareOp negate bool } | | | 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | type expValue struct { value string op compareOp negate bool } // AddExpr adds a match expression to the search. func (s *Search) AddExpr(key, val string) *Search { val, negate, op := parseOp(strings.TrimSpace(val)) if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() |
︙ | ︙ | |||
127 128 129 130 131 132 133 | return s[1:], negate, cmpSuffix case '~': return s[1:], negate, cmpContains } return s, negate, cmpDefault } | | | | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | return s[1:], negate, cmpSuffix case '~': return s[1:], negate, cmpContains } return s, negate, cmpDefault } // SetNegate changes the search to reverse its selection. func (s *Search) SetNegate() *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() s.negate = true return s } // AddPreMatch adds the pre-selection predicate. func (s *Search) AddPreMatch(preMatch MetaMatchFunc) *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if pre := s.preMatch; pre == nil { |
︙ | ︙ | |||
218 219 220 221 222 223 224 | return 0 } s.mx.RLock() defer s.mx.RUnlock() return s.limit } | | | | | | | | | 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 | return 0 } s.mx.RLock() defer s.mx.RUnlock() return s.limit } // HasComputedMetaKey returns true, if the search references a metadata key which // a computed value. func (s *Search) HasComputedMetaKey() bool { if s == nil { return false } s.mx.RLock() defer s.mx.RUnlock() for key := range s.tags { if meta.IsComputed(key) { return true } } if order := s.order; order != "" && meta.IsComputed(order) { return true } return false } // CompileMatch returns a function to match meta data based on select specification. func (s *Search) CompileMatch(searcher Searcher) MetaMatchFunc { if s == nil { return selectNone } s.mx.Lock() defer s.mx.Unlock() compMeta := compileSelect(s.tags) compSearch := compileFullSearch(searcher, s.search) if preMatch := s.preMatch; preMatch != nil { return compilePreMatch(preMatch, compMeta, compSearch, s.negate) } return compileNoPreMatch(compMeta, compSearch, s.negate) } func selectNone(m *meta.Meta) bool { return true } func compilePreMatch(preMatch, compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc { if compMeta == nil { if compSearch == nil { return preMatch } if negate { |
︙ | ︙ | |||
283 284 285 286 287 288 289 | func compileNoPreMatch(compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc { if compMeta == nil { if compSearch == nil { if negate { return func(m *meta.Meta) bool { return false } } | | | 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | func compileNoPreMatch(compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc { if compMeta == nil { if compSearch == nil { if negate { return func(m *meta.Meta) bool { return false } } return selectNone } if negate { return func(m *meta.Meta) bool { return !compSearch(m) } } return compSearch } if compSearch == nil { |
︙ | ︙ |
Added search/select.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package search provides a zettel search. package search import ( "strings" "zettelstore.de/z/domain/meta" ) type matchFunc func(value string) bool func matchNever(value string) bool { return false } func matchAlways(value string) bool { return true } type matchSpec struct { key string match matchFunc } // compileSelect calculates a selection func based on the given select criteria. func compileSelect(tags expTagValues) MetaMatchFunc { posSpecs, negSpecs, nomatch := createSelectSpecs(tags) if len(posSpecs) > 0 || len(negSpecs) > 0 || len(nomatch) > 0 { return makeSearchMetaMatchFunc(posSpecs, negSpecs, nomatch) } return nil } func createSelectSpecs(tags map[string][]expValue) (posSpecs, negSpecs []matchSpec, nomatch []string) { posSpecs = make([]matchSpec, 0, len(tags)) negSpecs = make([]matchSpec, 0, len(tags)) for key, values := range tags { if !meta.KeyIsValid(key) { continue } if always, never := countEmptyValues(values); always+never > 0 { if never == 0 { posSpecs = append(posSpecs, matchSpec{key, matchAlways}) continue } if always == 0 { negSpecs = append(negSpecs, matchSpec{key, nil}) continue } // value must match always AND never, at the same time. This results in a no-match. nomatch = append(nomatch, key) continue } posMatch, negMatch := createPosNegMatchFunc(key, values) if posMatch != nil { posSpecs = append(posSpecs, matchSpec{key, posMatch}) } if negMatch != nil { negSpecs = append(negSpecs, matchSpec{key, negMatch}) } } return posSpecs, negSpecs, nomatch } func countEmptyValues(values []expValue) (always, never int) { for _, v := range values { if v.value != "" { continue } if v.negate { never++ } else { always++ } } return always, never } func createPosNegMatchFunc(key string, values []expValue) (posMatch, negMatch matchFunc) { posValues := make([]opValue, 0, len(values)) negValues := make([]opValue, 0, len(values)) for _, val := range values { if val.negate { negValues = append(negValues, opValue{value: val.value, op: val.op}) } else { posValues = append(posValues, opValue{value: val.value, op: val.op}) } } return createMatchFunc(key, posValues), createMatchFunc(key, negValues) } // opValue is an expValue, but w/o the field "negate" type opValue struct { value string op compareOp } func createMatchFunc(key string, values []opValue) matchFunc { if len(values) == 0 { return nil } switch meta.Type(key) { case meta.TypeBool: return createMatchBoolFunc(values) case meta.TypeCredential: return matchNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout return createMatchIDFunc(values) case meta.TypeIDSet: return createMatchIDSetFunc(values) case meta.TypeTagSet: return createMatchTagSetFunc(values) case meta.TypeWord: return createMatchWordFunc(values) case meta.TypeWordSet: return createMatchWordSetFunc(values) } return createMatchStringFunc(values) } func createMatchBoolFunc(values []opValue) matchFunc { preValues := make([]bool, 0, len(values)) for _, v := range values { preValues = append(preValues, meta.BoolValue(v.value)) } return func(value string) bool { bValue := meta.BoolValue(value) for _, v := range preValues { if bValue != v { return false } } return true } } func createMatchIDFunc(values []opValue) matchFunc { return func(value string) bool { for _, v := range values { if !strings.HasPrefix(value, v.value) { return false } } return true } } func createMatchIDSetFunc(values []opValue) matchFunc { idValues := preprocessSet(sliceToLower(values)) return func(value string) bool { ids := meta.ListFromValue(value) for _, neededIDs := range idValues { for _, neededID := range neededIDs { if !matchAllID(ids, neededID.value) { return false } } } return true } } func matchAllID(zettelIDs []string, neededID string) bool { for _, zt := range zettelIDs { if strings.HasPrefix(zt, neededID) { return true } } return false } func createMatchTagSetFunc(values []opValue) matchFunc { tagValues := processTagSet(preprocessSet(values)) return func(value string) bool { tags := meta.ListFromValue(value) // Remove leading '#' from each tag for i, tag := range tags { tags[i] = meta.CleanTag(tag) } for _, neededTags := range tagValues { for _, neededTag := range neededTags { if !matchAllTag(tags, neededTag.value, neededTag.equal) { return false } } } return true } } type tagQueryValue struct { value string equal bool // not equal == prefix } func processTagSet(valueSet [][]opValue) [][]tagQueryValue { result := make([][]tagQueryValue, len(valueSet)) for i, values := range valueSet { tags := make([]tagQueryValue, len(values)) for j, val := range values { if tval := val.value; tval != "" && tval[0] == '#' { tval = meta.CleanTag(tval) tags[j] = tagQueryValue{value: tval, equal: true} } else { tags[j] = tagQueryValue{value: tval, equal: false} } } result[i] = tags } return result } func matchAllTag(zettelTags []string, neededTag string, equal bool) bool { if equal { for _, zt := range zettelTags { if zt == neededTag { return true } } } else { for _, zt := range zettelTags { if strings.HasPrefix(zt, neededTag) { return true } } } return false } func createMatchWordFunc(values []opValue) matchFunc { values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { if value != v.value { return false } } return true } } func createMatchWordSetFunc(values []opValue) matchFunc { wordValues := preprocessSet(sliceToLower(values)) return func(value string) bool { words := meta.ListFromValue(value) for _, neededWords := range wordValues { for _, neededWord := range neededWords { if !matchAllWord(words, neededWord.value) { return false } } } return true } } func createMatchStringFunc(values []opValue) matchFunc { values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { if !strings.Contains(value, v.value) { return false } } return true } } func makeSearchMetaMatchFunc(posSpecs, negSpecs []matchSpec, nomatch []string) MetaMatchFunc { return func(m *meta.Meta) bool { for _, key := range nomatch { if _, ok := m.Get(key); ok { return false } } for _, s := range posSpecs { if value, ok := m.Get(s.key); !ok || !s.match(value) { return false } } for _, s := range negSpecs { if s.match == nil { if _, ok := m.Get(s.key); ok { return false } } else if value, ok := m.Get(s.key); !ok || s.match(value) { return false } } return true } } func sliceToLower(sl []opValue) []opValue { result := make([]opValue, 0, len(sl)) for _, s := range sl { result = append(result, opValue{ value: strings.ToLower(s.value), op: s.op, }) } return result } func preprocessSet(set []opValue) [][]opValue { result := make([][]opValue, 0, len(set)) for _, elem := range set { splitElems := strings.Split(elem.value, ",") valueElems := make([]opValue, 0, len(splitElems)) for _, se := range splitElems { e := strings.TrimSpace(se) if len(e) > 0 { valueElems = append(valueElems, opValue{value: e, op: elem.op}) } } if len(valueElems) > 0 { result = append(result, valueElems) } } return result } func matchAllWord(zettelWords []string, neededWord string) bool { for _, zw := range zettelWords { if zw == neededWord { return true } } return false } |
Deleted search/selector.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to strfun/slugify_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "testing" "zettelstore.de/z/strfun" ) func TestSlugify(t *testing.T) { tests := []struct{ in, exp string }{ {"simple test", "simple-test"}, {"I'm a go developer", "i-m-a-go-developer"}, {"-!->simple test<-!-", "simple-test"}, {"äöüÄÖÜß", "aouaouß"}, {"\"aèf", "aef"}, {"a#b", "a-b"}, | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "testing" "zettelstore.de/z/strfun" ) func TestSlugify(t *testing.T) { t.Parallel() 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"}, |
︙ | ︙ | |||
43 44 45 46 47 48 49 50 51 52 53 54 55 56 | return false } } return true } func TestNormalizeWord(t *testing.T) { tests := []struct { in string exp []string }{ {"", []string{}}, {" ", []string{}}, {"ˋ", []string{}}, // No single diacritic char, such as U+02CB | > | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | return false } } return true } func TestNormalizeWord(t *testing.T) { t.Parallel() tests := []struct { in string exp []string }{ {"", []string{}}, {" ", []string{}}, {"ˋ", []string{}}, // No single diacritic char, such as U+02CB |
︙ | ︙ |
Changes to strfun/strfun_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "testing" "zettelstore.de/z/strfun" ) func TestTrimSpaceRight(t *testing.T) { const space = "\t\v\r\f\n\u0085\u00a0\u2000\u3000" testcases := []struct { in string exp string }{ {"", ""}, {"abc", "abc"}, | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "testing" "zettelstore.de/z/strfun" ) func TestTrimSpaceRight(t *testing.T) { t.Parallel() const space = "\t\v\r\f\n\u0085\u00a0\u2000\u3000" testcases := []struct { in string exp string }{ {"", ""}, {"abc", "abc"}, |
︙ | ︙ | |||
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | if got != tc.exp { t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got) } } } func TestLength(t *testing.T) { testcases := []struct { in string exp int }{ {"", 0}, {"äbc", 3}, } for i, tc := range testcases { got := strfun.Length(tc.in) if got != tc.exp { t.Errorf("%d/%q: expected %v, got %v", i, tc.in, tc.exp, got) } } } func TestJustifyLeft(t *testing.T) { testcases := []struct { in string ml int exp string }{ {"", 0, ""}, {"äbc", 0, ""}, | > > | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | if got != tc.exp { t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got) } } } func TestLength(t *testing.T) { t.Parallel() testcases := []struct { in string exp int }{ {"", 0}, {"äbc", 3}, } for i, tc := range testcases { got := strfun.Length(tc.in) if got != tc.exp { t.Errorf("%d/%q: expected %v, got %v", i, tc.in, tc.exp, got) } } } func TestJustifyLeft(t *testing.T) { t.Parallel() testcases := []struct { in string ml int exp string }{ {"", 0, ""}, {"äbc", 0, ""}, |
︙ | ︙ | |||
83 84 85 86 87 88 89 90 91 92 93 94 95 96 | if got != tc.exp { t.Errorf("%d/%q/%d: expected %q, got %q", i, tc.in, tc.ml, tc.exp, got) } } } func TestSplitLines(t *testing.T) { testcases := []struct { in string exp []string }{ {"", nil}, {"\n", nil}, {"a", []string{"a"}}, | > | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | if got != tc.exp { t.Errorf("%d/%q/%d: expected %q, got %q", i, tc.in, tc.ml, tc.exp, got) } } } func TestSplitLines(t *testing.T) { t.Parallel() testcases := []struct { in string exp []string }{ {"", nil}, {"\n", nil}, {"a", []string{"a"}}, |
︙ | ︙ |
Changes to template/mustache_test.go.
︙ | ︙ | |||
225 226 227 228 229 230 231 232 233 234 235 236 237 238 | 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) } | > | 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | if errMissing { tmpl.SetErrorOnMissing() } return render(tmpl, value) } func TestBasic(t *testing.T) { t.Parallel() for _, test := range tests { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } |
︙ | ︙ | |||
257 258 259 260 261 262 263 264 265 266 267 268 269 270 | //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) } | > | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | //dotted names(dot notation) {`"{{a.b.c}}" == ""`, map[string]interface{}{}, `"" == ""`, nil}, {`"{{a.b.c.name}}" == ""`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "c": map[string]string{"name": "Jim"}}, `"" == ""`, nil}, {`{{#a}}{{b.c}}{{/a}}`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "b": map[string]string{"c": "ERROR"}}, "", nil}, } func TestMissing(t *testing.T) { t.Parallel() for _, test := range missing { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Error(err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } |
︙ | ︙ | |||
287 288 289 290 291 292 293 294 295 296 297 298 299 300 | {`{{}`, 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()) | > | 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | {`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, {`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, //invalid syntax - https://github.com/hoisie/mustache/issues/10 {`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")}, } func TestMalformed(t *testing.T) { t.Parallel() for _, test := range malformed { output, err := renderString(test.tmpl, false, test.context) if err != nil { if test.err == nil { t.Error(err) } else if test.err.Error() != err.Error() { t.Errorf("%q expected error %q but got error %q", test.tmpl, test.err.Error(), err.Error()) |
︙ | ︙ | |||
319 320 321 322 323 324 325 326 327 328 329 330 331 332 | } 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 }{ { | > | 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | } func (p Person) Name2() string { return p.FirstName + " " + p.LastName } func TestPointerReceiver(t *testing.T) { t.Parallel() p := Person{"John", "Smith"} tests := []struct { tmpl string context interface{} expected string }{ { |
︙ | ︙ | |||
409 410 411 412 413 414 415 416 417 418 419 420 421 422 | }, }, }, }, } func TestTags(t *testing.T) { for _, test := range tagTests { testTags(t, &test) } } func testTags(t *testing.T, test *tagsTest) { tmpl, err := parseString(test.tmpl) | > | 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 | }, }, }, }, } func TestTags(t *testing.T) { t.Parallel() for _, test := range tagTests { testTags(t, &test) } } func testTags(t *testing.T, test *tagsTest) { tmpl, err := parseString(test.tmpl) |
︙ | ︙ |
Changes to template/spec_test.go.
︙ | ︙ | |||
178 179 180 181 182 183 184 185 186 187 188 189 190 191 | if err != nil { curDir = os.Getenv("PWD") } return filepath.Join(curDir, "..", "testdata", "mustache") } func TestSpec(t *testing.T) { root := getRoot() if _, err := os.Stat(root); err != nil { if errors.Is(err, os.ErrNotExist) { t.Fatalf("Could not find the mustache testdata folder at %s'", root) } t.Fatal(err) } | > | 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | if err != nil { curDir = os.Getenv("PWD") } return filepath.Join(curDir, "..", "testdata", "mustache") } func TestSpec(t *testing.T) { t.Parallel() root := getRoot() if _, err := os.Stat(root); err != nil { if errors.Is(err, os.ErrNotExist) { t.Fatalf("Could not find the mustache testdata folder at %s'", root) } t.Fatal(err) } |
︙ | ︙ |
Changes to testdata/markdown/README.md.
|
| | | 1 2 | File `spec.json` is the CommonMark specification 0.30 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 5195 5196 5197 5198 5199 5200 5201 5202 5203 5204 5205 5206 5207 5208 5209 5210 5211 5212 5213 5214 5215 5216 5217 5218 | [ { "markdown": "\tfoo\tbaz\t\tbim\n", "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n", "example": 1, "start_line": 356, "end_line": 361, "section": "Tabs" }, { "markdown": " \tfoo\tbaz\t\tbim\n", "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n", "example": 2, "start_line": 363, "end_line": 368, "section": "Tabs" }, { "markdown": " a\ta\n ὐ\ta\n", "html": "<pre><code>a\ta\nὐ\ta\n</code></pre>\n", "example": 3, "start_line": 370, "end_line": 377, "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": 383, "end_line": 394, "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": 396, "end_line": 408, "section": "Tabs" }, { "markdown": ">\t\tfoo\n", "html": "<blockquote>\n<pre><code> foo\n</code></pre>\n</blockquote>\n", "example": 6, "start_line": 419, "end_line": 426, "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": 428, "end_line": 437, "section": "Tabs" }, { "markdown": " foo\n\tbar\n", "html": "<pre><code>foo\nbar\n</code></pre>\n", "example": 8, "start_line": 440, "end_line": 447, "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": 449, "end_line": 465, "section": "Tabs" }, { "markdown": "#\tFoo\n", "html": "<h1>Foo</h1>\n", "example": 10, "start_line": 467, "end_line": 471, "section": "Tabs" }, { "markdown": "*\t*\t*\t\n", "html": "<hr />\n", "example": 11, "start_line": 473, "end_line": 477, "section": "Tabs" }, { "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n", "html": "<p>!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~</p>\n", "example": 12, "start_line": 490, "end_line": 494, "section": "Backslash escapes" }, { "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n", "html": "<p>\\\t\\A\\a\\ \\3\\φ\\«</p>\n", "example": 13, "start_line": 500, "end_line": 504, "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": 14, "start_line": 510, "end_line": 530, "section": "Backslash escapes" }, { "markdown": "\\\\*emphasis*\n", "html": "<p>\\<em>emphasis</em></p>\n", "example": 15, "start_line": 535, "end_line": 539, "section": "Backslash escapes" }, { "markdown": "foo\\\nbar\n", "html": "<p>foo<br />\nbar</p>\n", "example": 16, "start_line": 544, "end_line": 550, "section": "Backslash escapes" }, { "markdown": "`` \\[\\` ``\n", "html": "<p><code>\\[\\`</code></p>\n", "example": 17, "start_line": 556, "end_line": 560, "section": "Backslash escapes" }, { "markdown": " \\[\\]\n", "html": "<pre><code>\\[\\]\n</code></pre>\n", "example": 18, "start_line": 563, "end_line": 568, "section": "Backslash escapes" }, { "markdown": "~~~\n\\[\\]\n~~~\n", "html": "<pre><code>\\[\\]\n</code></pre>\n", "example": 19, "start_line": 571, "end_line": 578, "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": 20, "start_line": 581, "end_line": 585, "section": "Backslash escapes" }, { "markdown": "<a href=\"/bar\\/)\">\n", "html": "<a href=\"/bar\\/)\">\n", "example": 21, "start_line": 588, "end_line": 592, "section": "Backslash escapes" }, { "markdown": "[foo](/bar\\* \"ti\\*tle\")\n", "html": "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n", "example": 22, "start_line": 598, "end_line": 602, "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": 23, "start_line": 605, "end_line": 611, "section": "Backslash escapes" }, { "markdown": "``` foo\\+bar\nfoo\n```\n", "html": "<pre><code class=\"language-foo+bar\">foo\n</code></pre>\n", "example": 24, "start_line": 614, "end_line": 621, "section": "Backslash escapes" }, { "markdown": " & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n", "html": "<p> & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸</p>\n", "example": 25, "start_line": 650, "end_line": 658, "section": "Entity and numeric character references" }, { "markdown": "# Ӓ Ϡ �\n", "html": "<p># Ӓ Ϡ �</p>\n", "example": 26, "start_line": 669, "end_line": 673, "section": "Entity and numeric character references" }, { "markdown": "" ആ ಫ\n", "html": "<p>" ആ ಫ</p>\n", "example": 27, "start_line": 682, "end_line": 686, "section": "Entity and numeric character references" }, { "markdown": "  &x; &#; &#x;\n�\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n", "html": "<p>&nbsp &x; &#; &#x;\n&#87654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;</p>\n", "example": 28, "start_line": 691, "end_line": 701, "section": "Entity and numeric character references" }, { "markdown": "©\n", "html": "<p>&copy</p>\n", "example": 29, "start_line": 708, "end_line": 712, "section": "Entity and numeric character references" }, { "markdown": "&MadeUpEntity;\n", "html": "<p>&MadeUpEntity;</p>\n", "example": 30, "start_line": 718, "end_line": 722, "section": "Entity and numeric character references" }, { "markdown": "<a href=\"öö.html\">\n", "html": "<a href=\"öö.html\">\n", "example": 31, "start_line": 729, "end_line": 733, "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": 32, "start_line": 736, "end_line": 740, "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": 33, "start_line": 743, "end_line": 749, "section": "Entity and numeric character references" }, { "markdown": "``` föö\nfoo\n```\n", "html": "<pre><code class=\"language-föö\">foo\n</code></pre>\n", "example": 34, "start_line": 752, "end_line": 759, "section": "Entity and numeric character references" }, { "markdown": "`föö`\n", "html": "<p><code>f&ouml;&ouml;</code></p>\n", "example": 35, "start_line": 765, "end_line": 769, "section": "Entity and numeric character references" }, { "markdown": " föfö\n", "html": "<pre><code>f&ouml;f&ouml;\n</code></pre>\n", "example": 36, "start_line": 772, "end_line": 777, "section": "Entity and numeric character references" }, { "markdown": "*foo*\n*foo*\n", "html": "<p>*foo*\n<em>foo</em></p>\n", "example": 37, "start_line": 784, "end_line": 790, "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": 38, "start_line": 792, "end_line": 801, "section": "Entity and numeric character references" }, { "markdown": "foo bar\n", "html": "<p>foo\n\nbar</p>\n", "example": 39, "start_line": 803, "end_line": 809, "section": "Entity and numeric character references" }, { "markdown": "	foo\n", "html": "<p>\tfoo</p>\n", "example": 40, "start_line": 811, "end_line": 815, "section": "Entity and numeric character references" }, { "markdown": "[a](url "tit")\n", "html": "<p>[a](url "tit")</p>\n", "example": 41, "start_line": 818, "end_line": 822, "section": "Entity and numeric character references" }, { "markdown": "- `one\n- two`\n", "html": "<ul>\n<li>`one</li>\n<li>two`</li>\n</ul>\n", "example": 42, "start_line": 841, "end_line": 849, "section": "Precedence" }, { "markdown": "***\n---\n___\n", "html": "<hr />\n<hr />\n<hr />\n", "example": 43, "start_line": 880, "end_line": 888, "section": "Thematic breaks" }, { "markdown": "+++\n", "html": "<p>+++</p>\n", "example": 44, "start_line": 893, "end_line": 897, "section": "Thematic breaks" }, { "markdown": "===\n", "html": "<p>===</p>\n", "example": 45, "start_line": 900, "end_line": 904, "section": "Thematic breaks" }, { "markdown": "--\n**\n__\n", "html": "<p>--\n**\n__</p>\n", "example": 46, "start_line": 909, "end_line": 917, "section": "Thematic breaks" }, { "markdown": " ***\n ***\n ***\n", "html": "<hr />\n<hr />\n<hr />\n", "example": 47, "start_line": 922, "end_line": 930, "section": "Thematic breaks" }, { "markdown": " ***\n", "html": "<pre><code>***\n</code></pre>\n", "example": 48, "start_line": 935, "end_line": 940, "section": "Thematic breaks" }, { "markdown": "Foo\n ***\n", "html": "<p>Foo\n***</p>\n", "example": 49, "start_line": 943, "end_line": 949, "section": "Thematic breaks" }, { "markdown": "_____________________________________\n", "html": "<hr />\n", "example": 50, "start_line": 954, "end_line": 958, "section": "Thematic breaks" }, { "markdown": " - - -\n", "html": "<hr />\n", "example": 51, "start_line": 963, "end_line": 967, "section": "Thematic breaks" }, { "markdown": " ** * ** * ** * **\n", "html": "<hr />\n", "example": 52, "start_line": 970, "end_line": 974, "section": "Thematic breaks" }, { "markdown": "- - - -\n", "html": "<hr />\n", "example": 53, "start_line": 977, "end_line": 981, "section": "Thematic breaks" }, { "markdown": "- - - - \n", "html": "<hr />\n", "example": 54, "start_line": 986, "end_line": 990, "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": 55, "start_line": 995, "end_line": 1005, "section": "Thematic breaks" }, { "markdown": " *-*\n", "html": "<p><em>-</em></p>\n", "example": 56, "start_line": 1011, "end_line": 1015, "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": 57, "start_line": 1020, "end_line": 1032, "section": "Thematic breaks" }, { "markdown": "Foo\n***\nbar\n", "html": "<p>Foo</p>\n<hr />\n<p>bar</p>\n", "example": 58, "start_line": 1037, "end_line": 1045, "section": "Thematic breaks" }, { "markdown": "Foo\n---\nbar\n", "html": "<h2>Foo</h2>\n<p>bar</p>\n", "example": 59, "start_line": 1054, "end_line": 1061, "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": 60, "start_line": 1067, "end_line": 1079, "section": "Thematic breaks" }, { "markdown": "- Foo\n- * * *\n", "html": "<ul>\n<li>Foo</li>\n<li>\n<hr />\n</li>\n</ul>\n", "example": 61, "start_line": 1084, "end_line": 1094, "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": 62, "start_line": 1113, "end_line": 1127, "section": "ATX headings" }, { "markdown": "####### foo\n", "html": "<p>####### foo</p>\n", "example": 63, "start_line": 1132, "end_line": 1136, "section": "ATX headings" }, { "markdown": "#5 bolt\n\n#hashtag\n", "html": "<p>#5 bolt</p>\n<p>#hashtag</p>\n", "example": 64, "start_line": 1147, "end_line": 1154, "section": "ATX headings" }, { "markdown": "\\## foo\n", "html": "<p>## foo</p>\n", "example": 65, "start_line": 1159, "end_line": 1163, "section": "ATX headings" }, { "markdown": "# foo *bar* \\*baz\\*\n", "html": "<h1>foo <em>bar</em> *baz*</h1>\n", "example": 66, "start_line": 1168, "end_line": 1172, "section": "ATX headings" }, { "markdown": "# foo \n", "html": "<h1>foo</h1>\n", "example": 67, "start_line": 1177, "end_line": 1181, "section": "ATX headings" }, { "markdown": " ### foo\n ## foo\n # foo\n", "html": "<h3>foo</h3>\n<h2>foo</h2>\n<h1>foo</h1>\n", "example": 68, "start_line": 1186, "end_line": 1194, "section": "ATX headings" }, { "markdown": " # foo\n", "html": "<pre><code># foo\n</code></pre>\n", "example": 69, "start_line": 1199, "end_line": 1204, "section": "ATX headings" }, { "markdown": "foo\n # bar\n", "html": "<p>foo\n# bar</p>\n", "example": 70, "start_line": 1207, "end_line": 1213, "section": "ATX headings" }, { "markdown": "## foo ##\n ### bar ###\n", "html": "<h2>foo</h2>\n<h3>bar</h3>\n", "example": 71, "start_line": 1218, "end_line": 1224, "section": "ATX headings" }, { "markdown": "# foo ##################################\n##### foo ##\n", "html": "<h1>foo</h1>\n<h5>foo</h5>\n", "example": 72, "start_line": 1229, "end_line": 1235, "section": "ATX headings" }, { "markdown": "### foo ### \n", "html": "<h3>foo</h3>\n", "example": 73, "start_line": 1240, "end_line": 1244, "section": "ATX headings" }, { "markdown": "### foo ### b\n", "html": "<h3>foo ### b</h3>\n", "example": 74, "start_line": 1251, "end_line": 1255, "section": "ATX headings" }, { "markdown": "# foo#\n", "html": "<h1>foo#</h1>\n", "example": 75, "start_line": 1260, "end_line": 1264, "section": "ATX headings" }, { "markdown": "### foo \\###\n## foo #\\##\n# foo \\#\n", "html": "<h3>foo ###</h3>\n<h2>foo ###</h2>\n<h1>foo #</h1>\n", "example": 76, "start_line": 1270, "end_line": 1278, "section": "ATX headings" }, { "markdown": "****\n## foo\n****\n", "html": "<hr />\n<h2>foo</h2>\n<hr />\n", "example": 77, "start_line": 1284, "end_line": 1292, "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": 78, "start_line": 1295, "end_line": 1303, "section": "ATX headings" }, { "markdown": "## \n#\n### ###\n", "html": "<h2></h2>\n<h1></h1>\n<h3></h3>\n", "example": 79, "start_line": 1308, "end_line": 1316, "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": 80, "start_line": 1351, "end_line": 1360, "section": "Setext headings" }, { "markdown": "Foo *bar\nbaz*\n====\n", "html": "<h1>Foo <em>bar\nbaz</em></h1>\n", "example": 81, "start_line": 1365, "end_line": 1372, "section": "Setext headings" }, { "markdown": " Foo *bar\nbaz*\t\n====\n", "html": "<h1>Foo <em>bar\nbaz</em></h1>\n", "example": 82, "start_line": 1379, "end_line": 1386, "section": "Setext headings" }, { "markdown": "Foo\n-------------------------\n\nFoo\n=\n", "html": "<h2>Foo</h2>\n<h1>Foo</h1>\n", "example": 83, "start_line": 1391, "end_line": 1400, "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": 84, "start_line": 1406, "end_line": 1419, "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": 85, "start_line": 1424, "end_line": 1437, "section": "Setext headings" }, { "markdown": "Foo\n ---- \n", "html": "<h2>Foo</h2>\n", "example": 86, "start_line": 1443, "end_line": 1448, "section": "Setext headings" }, { "markdown": "Foo\n ---\n", "html": "<p>Foo\n---</p>\n", "example": 87, "start_line": 1453, "end_line": 1459, "section": "Setext headings" }, { "markdown": "Foo\n= =\n\nFoo\n--- -\n", "html": "<p>Foo\n= =</p>\n<p>Foo</p>\n<hr />\n", "example": 88, "start_line": 1464, "end_line": 1475, "section": "Setext headings" }, { "markdown": "Foo \n-----\n", "html": "<h2>Foo</h2>\n", "example": 89, "start_line": 1480, "end_line": 1485, "section": "Setext headings" }, { "markdown": "Foo\\\n----\n", "html": "<h2>Foo\\</h2>\n", "example": 90, "start_line": 1490, "end_line": 1495, "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": 91, "start_line": 1501, "end_line": 1514, "section": "Setext headings" }, { "markdown": "> Foo\n---\n", "html": "<blockquote>\n<p>Foo</p>\n</blockquote>\n<hr />\n", "example": 92, "start_line": 1520, "end_line": 1528, "section": "Setext headings" }, { "markdown": "> foo\nbar\n===\n", "html": "<blockquote>\n<p>foo\nbar\n===</p>\n</blockquote>\n", "example": 93, "start_line": 1531, "end_line": 1541, "section": "Setext headings" }, { "markdown": "- Foo\n---\n", "html": "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n", "example": 94, "start_line": 1544, "end_line": 1552, "section": "Setext headings" }, { "markdown": "Foo\nBar\n---\n", "html": "<h2>Foo\nBar</h2>\n", "example": 95, "start_line": 1559, "end_line": 1566, "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": 96, "start_line": 1572, "end_line": 1584, "section": "Setext headings" }, { "markdown": "\n====\n", "html": "<p>====</p>\n", "example": 97, "start_line": 1589, "end_line": 1594, "section": "Setext headings" }, { "markdown": "---\n---\n", "html": "<hr />\n<hr />\n", "example": 98, "start_line": 1601, "end_line": 1607, "section": "Setext headings" }, { "markdown": "- foo\n-----\n", "html": "<ul>\n<li>foo</li>\n</ul>\n<hr />\n", "example": 99, "start_line": 1610, "end_line": 1618, "section": "Setext headings" }, { "markdown": " foo\n---\n", "html": "<pre><code>foo\n</code></pre>\n<hr />\n", "example": 100, "start_line": 1621, "end_line": 1628, "section": "Setext headings" }, { "markdown": "> foo\n-----\n", "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n", "example": 101, "start_line": 1631, "end_line": 1639, "section": "Setext headings" }, { "markdown": "\\> foo\n------\n", "html": "<h2>> foo</h2>\n", "example": 102, "start_line": 1645, "end_line": 1650, "section": "Setext headings" }, { "markdown": "Foo\n\nbar\n---\nbaz\n", "html": "<p>Foo</p>\n<h2>bar</h2>\n<p>baz</p>\n", "example": 103, "start_line": 1676, "end_line": 1686, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\n---\n\nbaz\n", "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n", "example": 104, "start_line": 1692, "end_line": 1704, "section": "Setext headings" }, { "markdown": "Foo\nbar\n* * *\nbaz\n", "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n", "example": 105, "start_line": 1710, "end_line": 1720, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\\---\nbaz\n", "html": "<p>Foo\nbar\n---\nbaz</p>\n", "example": 106, "start_line": 1725, "end_line": 1735, "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": 107, "start_line": 1753, "end_line": 1760, "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": 108, "start_line": 1767, "end_line": 1778, "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": 109, "start_line": 1781, "end_line": 1794, "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": 110, "start_line": 1801, "end_line": 1812, "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": 111, "start_line": 1817, "end_line": 1834, "section": "Indented code blocks" }, { "markdown": " chunk1\n \n chunk2\n", "html": "<pre><code>chunk1\n \n chunk2\n</code></pre>\n", "example": 112, "start_line": 1840, "end_line": 1849, "section": "Indented code blocks" }, { "markdown": "Foo\n bar\n\n", "html": "<p>Foo\nbar</p>\n", "example": 113, "start_line": 1855, "end_line": 1862, "section": "Indented code blocks" }, { "markdown": " foo\nbar\n", "html": "<pre><code>foo\n</code></pre>\n<p>bar</p>\n", "example": 114, "start_line": 1869, "end_line": 1876, "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": 115, "start_line": 1882, "end_line": 1897, "section": "Indented code blocks" }, { "markdown": " foo\n bar\n", "html": "<pre><code> foo\nbar\n</code></pre>\n", "example": 116, "start_line": 1902, "end_line": 1909, "section": "Indented code blocks" }, { "markdown": "\n \n foo\n \n\n", "html": "<pre><code>foo\n</code></pre>\n", "example": 117, "start_line": 1915, "end_line": 1924, "section": "Indented code blocks" }, { "markdown": " foo \n", "html": "<pre><code>foo \n</code></pre>\n", "example": 118, "start_line": 1929, "end_line": 1934, "section": "Indented code blocks" }, { "markdown": "```\n<\n >\n```\n", "html": "<pre><code><\n >\n</code></pre>\n", "example": 119, "start_line": 1984, "end_line": 1993, "section": "Fenced code blocks" }, { "markdown": "~~~\n<\n >\n~~~\n", "html": "<pre><code><\n >\n</code></pre>\n", "example": 120, "start_line": 1998, "end_line": 2007, "section": "Fenced code blocks" }, { "markdown": "``\nfoo\n``\n", "html": "<p><code>foo</code></p>\n", "example": 121, "start_line": 2011, "end_line": 2017, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n~~~\n```\n", "html": "<pre><code>aaa\n~~~\n</code></pre>\n", "example": 122, "start_line": 2022, "end_line": 2031, "section": "Fenced code blocks" }, { "markdown": "~~~\naaa\n```\n~~~\n", "html": "<pre><code>aaa\n```\n</code></pre>\n", "example": 123, "start_line": 2034, "end_line": 2043, "section": "Fenced code blocks" }, { "markdown": "````\naaa\n```\n``````\n", "html": "<pre><code>aaa\n```\n</code></pre>\n", "example": 124, "start_line": 2048, "end_line": 2057, "section": "Fenced code blocks" }, { "markdown": "~~~~\naaa\n~~~\n~~~~\n", "html": "<pre><code>aaa\n~~~\n</code></pre>\n", "example": 125, "start_line": 2060, "end_line": 2069, "section": "Fenced code blocks" }, { "markdown": "```\n", "html": "<pre><code></code></pre>\n", "example": 126, "start_line": 2075, "end_line": 2079, "section": "Fenced code blocks" }, { "markdown": "`````\n\n```\naaa\n", "html": "<pre><code>\n```\naaa\n</code></pre>\n", "example": 127, "start_line": 2082, "end_line": 2092, "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": 128, "start_line": 2095, "end_line": 2106, "section": "Fenced code blocks" }, { "markdown": "```\n\n \n```\n", "html": "<pre><code>\n \n</code></pre>\n", "example": 129, "start_line": 2111, "end_line": 2120, "section": "Fenced code blocks" }, { "markdown": "```\n```\n", "html": "<pre><code></code></pre>\n", "example": 130, "start_line": 2125, "end_line": 2130, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\naaa\n```\n", "html": "<pre><code>aaa\naaa\n</code></pre>\n", "example": 131, "start_line": 2137, "end_line": 2146, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n aaa\naaa\n ```\n", "html": "<pre><code>aaa\naaa\naaa\n</code></pre>\n", "example": 132, "start_line": 2149, "end_line": 2160, "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": 133, "start_line": 2163, "end_line": 2174, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\n ```\n", "html": "<pre><code>```\naaa\n```\n</code></pre>\n", "example": 134, "start_line": 2179, "end_line": 2188, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "<pre><code>aaa\n</code></pre>\n", "example": 135, "start_line": 2194, "end_line": 2201, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n ```\n", "html": "<pre><code>aaa\n</code></pre>\n", "example": 136, "start_line": 2204, "end_line": 2211, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "<pre><code>aaa\n ```\n</code></pre>\n", "example": 137, "start_line": 2216, "end_line": 2224, "section": "Fenced code blocks" }, { "markdown": "``` ```\naaa\n", "html": "<p><code> </code>\naaa</p>\n", "example": 138, "start_line": 2230, "end_line": 2236, "section": "Fenced code blocks" }, { "markdown": "~~~~~~\naaa\n~~~ ~~\n", "html": "<pre><code>aaa\n~~~ ~~\n</code></pre>\n", "example": 139, "start_line": 2239, "end_line": 2247, "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": 140, "start_line": 2253, "end_line": 2264, "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": 141, "start_line": 2270, "end_line": 2282, "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": 142, "start_line": 2292, "end_line": 2303, "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": 143, "start_line": 2306, "end_line": 2317, "section": "Fenced code blocks" }, { "markdown": "````;\n````\n", "html": "<pre><code class=\"language-;\"></code></pre>\n", "example": 144, "start_line": 2320, "end_line": 2325, "section": "Fenced code blocks" }, { "markdown": "``` aa ```\nfoo\n", "html": "<p><code>aa</code>\nfoo</p>\n", "example": 145, "start_line": 2330, "end_line": 2336, "section": "Fenced code blocks" }, { "markdown": "~~~ aa ``` ~~~\nfoo\n~~~\n", "html": "<pre><code class=\"language-aa\">foo\n</code></pre>\n", "example": 146, "start_line": 2341, "end_line": 2348, "section": "Fenced code blocks" }, { "markdown": "```\n``` aaa\n```\n", "html": "<pre><code>``` aaa\n</code></pre>\n", "example": 147, "start_line": 2353, "end_line": 2360, "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": 148, "start_line": 2432, "end_line": 2447, "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": 149, "start_line": 2461, "end_line": 2480, "section": "HTML blocks" }, { "markdown": " <div>\n *hello*\n <foo><a>\n", "html": " <div>\n *hello*\n <foo><a>\n", "example": 150, "start_line": 2483, "end_line": 2491, "section": "HTML blocks" }, { "markdown": "</div>\n*foo*\n", "html": "</div>\n*foo*\n", "example": 151, "start_line": 2496, "end_line": 2502, "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": 152, "start_line": 2507, "end_line": 2517, "section": "HTML blocks" }, { "markdown": "<div id=\"foo\"\n class=\"bar\">\n</div>\n", "html": "<div id=\"foo\"\n class=\"bar\">\n</div>\n", "example": 153, "start_line": 2523, "end_line": 2531, "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": 154, "start_line": 2534, "end_line": 2542, "section": "HTML blocks" }, { "markdown": "<div>\n*foo*\n\n*bar*\n", "html": "<div>\n*foo*\n<p><em>bar</em></p>\n", "example": 155, "start_line": 2546, "end_line": 2555, "section": "HTML blocks" }, { "markdown": "<div id=\"foo\"\n*hi*\n", "html": "<div id=\"foo\"\n*hi*\n", "example": 156, "start_line": 2562, "end_line": 2568, "section": "HTML blocks" }, { "markdown": "<div class\nfoo\n", "html": "<div class\nfoo\n", "example": 157, "start_line": 2571, "end_line": 2577, "section": "HTML blocks" }, { "markdown": "<div *???-&&&-<---\n*foo*\n", "html": "<div *???-&&&-<---\n*foo*\n", "example": 158, "start_line": 2583, "end_line": 2589, "section": "HTML blocks" }, { "markdown": "<div><a href=\"bar\">*foo*</a></div>\n", "html": "<div><a href=\"bar\">*foo*</a></div>\n", "example": 159, "start_line": 2595, "end_line": 2599, "section": "HTML blocks" }, { "markdown": "<table><tr><td>\nfoo\n</td></tr></table>\n", "html": "<table><tr><td>\nfoo\n</td></tr></table>\n", "example": 160, "start_line": 2602, "end_line": 2610, "section": "HTML blocks" }, { "markdown": "<div></div>\n``` c\nint x = 33;\n```\n", "html": "<div></div>\n``` c\nint x = 33;\n```\n", "example": 161, "start_line": 2619, "end_line": 2629, "section": "HTML blocks" }, { "markdown": "<a href=\"foo\">\n*bar*\n</a>\n", "html": "<a href=\"foo\">\n*bar*\n</a>\n", "example": 162, "start_line": 2636, "end_line": 2644, "section": "HTML blocks" }, { "markdown": "<Warning>\n*bar*\n</Warning>\n", "html": "<Warning>\n*bar*\n</Warning>\n", "example": 163, "start_line": 2649, "end_line": 2657, "section": "HTML blocks" }, { "markdown": "<i class=\"foo\">\n*bar*\n</i>\n", "html": "<i class=\"foo\">\n*bar*\n</i>\n", "example": 164, "start_line": 2660, "end_line": 2668, "section": "HTML blocks" }, { "markdown": "</ins>\n*bar*\n", "html": "</ins>\n*bar*\n", "example": 165, "start_line": 2671, "end_line": 2677, "section": "HTML blocks" }, { "markdown": "<del>\n*foo*\n</del>\n", "html": "<del>\n*foo*\n</del>\n", "example": 166, "start_line": 2686, "end_line": 2694, "section": "HTML blocks" }, { "markdown": "<del>\n\n*foo*\n\n</del>\n", "html": "<del>\n<p><em>foo</em></p>\n</del>\n", "example": 167, "start_line": 2701, "end_line": 2711, "section": "HTML blocks" }, { "markdown": "<del>*foo*</del>\n", "html": "<p><del><em>foo</em></del></p>\n", "example": 168, "start_line": 2719, "end_line": 2723, "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": 169, "start_line": 2735, "end_line": 2751, "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": 170, "start_line": 2756, "end_line": 2770, "section": "HTML blocks" }, { "markdown": "<textarea>\n\n*foo*\n\n_bar_\n\n</textarea>\n", "html": "<textarea>\n\n*foo*\n\n_bar_\n\n</textarea>\n", "example": 171, "start_line": 2775, "end_line": 2791, "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": 172, "start_line": 2795, "end_line": 2811, "section": "HTML blocks" }, { "markdown": "<style\n type=\"text/css\">\n\nfoo\n", "html": "<style\n type=\"text/css\">\n\nfoo\n", "example": 173, "start_line": 2818, "end_line": 2828, "section": "HTML blocks" }, { "markdown": "> <div>\n> foo\n\nbar\n", "html": "<blockquote>\n<div>\nfoo\n</blockquote>\n<p>bar</p>\n", "example": 174, "start_line": 2831, "end_line": 2842, "section": "HTML blocks" }, { "markdown": "- <div>\n- foo\n", "html": "<ul>\n<li>\n<div>\n</li>\n<li>foo</li>\n</ul>\n", "example": 175, "start_line": 2845, "end_line": 2855, "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": 176, "start_line": 2860, "end_line": 2866, "section": "HTML blocks" }, { "markdown": "<!-- foo -->*bar*\n*baz*\n", "html": "<!-- foo -->*bar*\n<p><em>baz</em></p>\n", "example": 177, "start_line": 2869, "end_line": 2875, "section": "HTML blocks" }, { "markdown": "<script>\nfoo\n</script>1. *bar*\n", "html": "<script>\nfoo\n</script>1. *bar*\n", "example": 178, "start_line": 2881, "end_line": 2889, "section": "HTML blocks" }, { "markdown": "<!-- Foo\n\nbar\n baz -->\nokay\n", "html": "<!-- Foo\n\nbar\n baz -->\n<p>okay</p>\n", "example": 179, "start_line": 2894, "end_line": 2906, "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": 180, "start_line": 2912, "end_line": 2926, "section": "HTML blocks" }, { "markdown": "<!DOCTYPE html>\n", "html": "<!DOCTYPE html>\n", "example": 181, "start_line": 2931, "end_line": 2935, "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": 182, "start_line": 2940, "end_line": 2968, "section": "HTML blocks" }, { "markdown": " <!-- foo -->\n\n <!-- foo -->\n", "html": " <!-- foo -->\n<pre><code><!-- foo -->\n</code></pre>\n", "example": 183, "start_line": 2974, "end_line": 2982, "section": "HTML blocks" }, { "markdown": " <div>\n\n <div>\n", "html": " <div>\n<pre><code><div>\n</code></pre>\n", "example": 184, "start_line": 2985, "end_line": 2993, "section": "HTML blocks" }, { "markdown": "Foo\n<div>\nbar\n</div>\n", "html": "<p>Foo</p>\n<div>\nbar\n</div>\n", "example": 185, "start_line": 2999, "end_line": 3009, "section": "HTML blocks" }, { "markdown": "<div>\nbar\n</div>\n*foo*\n", "html": "<div>\nbar\n</div>\n*foo*\n", "example": 186, "start_line": 3016, "end_line": 3026, "section": "HTML blocks" }, { "markdown": "Foo\n<a href=\"bar\">\nbaz\n", "html": "<p>Foo\n<a href=\"bar\">\nbaz</p>\n", "example": 187, "start_line": 3031, "end_line": 3039, "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": 188, "start_line": 3072, "end_line": 3082, "section": "HTML blocks" }, { "markdown": "<div>\n*Emphasized* text.\n</div>\n", "html": "<div>\n*Emphasized* text.\n</div>\n", "example": 189, "start_line": 3085, "end_line": 3093, "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": 190, "start_line": 3107, "end_line": 3127, "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": 191, "start_line": 3134, "end_line": 3155, "section": "HTML blocks" }, { "markdown": "[foo]: /url \"title\"\n\n[foo]\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 192, "start_line": 3183, "end_line": 3189, "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": 193, "start_line": 3192, "end_line": 3200, "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": 194, "start_line": 3203, "end_line": 3209, "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": 195, "start_line": 3212, "end_line": 3220, "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": 196, "start_line": 3225, "end_line": 3239, "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": 197, "start_line": 3244, "end_line": 3254, "section": "Link reference definitions" }, { "markdown": "[foo]:\n/url\n\n[foo]\n", "html": "<p><a href=\"/url\">foo</a></p>\n", "example": 198, "start_line": 3259, "end_line": 3266, "section": "Link reference definitions" }, { "markdown": "[foo]:\n\n[foo]\n", "html": "<p>[foo]:</p>\n<p>[foo]</p>\n", "example": 199, "start_line": 3271, "end_line": 3278, "section": "Link reference definitions" }, { "markdown": "[foo]: <>\n\n[foo]\n", "html": "<p><a href=\"\">foo</a></p>\n", "example": 200, "start_line": 3283, "end_line": 3289, "section": "Link reference definitions" }, { "markdown": "[foo]: <bar>(baz)\n\n[foo]\n", "html": "<p>[foo]: <bar>(baz)</p>\n<p>[foo]</p>\n", "example": 201, "start_line": 3294, "end_line": 3301, "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": 202, "start_line": 3307, "end_line": 3313, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: url\n", "html": "<p><a href=\"url\">foo</a></p>\n", "example": 203, "start_line": 3318, "end_line": 3324, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n", "html": "<p><a href=\"first\">foo</a></p>\n", "example": 204, "start_line": 3330, "end_line": 3337, "section": "Link reference definitions" }, { "markdown": "[FOO]: /url\n\n[Foo]\n", "html": "<p><a href=\"/url\">Foo</a></p>\n", "example": 205, "start_line": 3343, "end_line": 3349, "section": "Link reference definitions" }, { "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n", "html": "<p><a href=\"/%CF%86%CE%BF%CF%85\">αγω</a></p>\n", "example": 206, "start_line": 3352, "end_line": 3358, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n", "html": "", "example": 207, "start_line": 3367, "end_line": 3370, "section": "Link reference definitions" }, { "markdown": "[\nfoo\n]: /url\nbar\n", "html": "<p>bar</p>\n", "example": 208, "start_line": 3375, "end_line": 3382, "section": "Link reference definitions" }, { "markdown": "[foo]: /url \"title\" ok\n", "html": "<p>[foo]: /url "title" ok</p>\n", "example": 209, "start_line": 3388, "end_line": 3392, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n\"title\" ok\n", "html": "<p>"title" ok</p>\n", "example": 210, "start_line": 3397, "end_line": 3402, "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": 211, "start_line": 3408, "end_line": 3416, "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": 212, "start_line": 3422, "end_line": 3432, "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": 213, "start_line": 3437, "end_line": 3446, "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": 214, "start_line": 3452, "end_line": 3461, "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": 215, "start_line": 3463, "end_line": 3471, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n===\n[foo]\n", "html": "<p>===\n<a href=\"/url\">foo</a></p>\n", "example": 216, "start_line": 3473, "end_line": 3480, "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": 217, "start_line": 3486, "end_line": 3499, "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": 218, "start_line": 3507, "end_line": 3515, "section": "Link reference definitions" }, { "markdown": "aaa\n\nbbb\n", "html": "<p>aaa</p>\n<p>bbb</p>\n", "example": 219, "start_line": 3529, "end_line": 3536, "section": "Paragraphs" }, { "markdown": "aaa\nbbb\n\nccc\nddd\n", "html": "<p>aaa\nbbb</p>\n<p>ccc\nddd</p>\n", "example": 220, "start_line": 3541, "end_line": 3552, "section": "Paragraphs" }, { "markdown": "aaa\n\n\nbbb\n", "html": "<p>aaa</p>\n<p>bbb</p>\n", "example": 221, "start_line": 3557, "end_line": 3565, "section": "Paragraphs" }, { "markdown": " aaa\n bbb\n", "html": "<p>aaa\nbbb</p>\n", "example": 222, "start_line": 3570, "end_line": 3576, "section": "Paragraphs" }, { "markdown": "aaa\n bbb\n ccc\n", "html": "<p>aaa\nbbb\nccc</p>\n", "example": 223, "start_line": 3582, "end_line": 3590, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "<p>aaa\nbbb</p>\n", "example": 224, "start_line": 3596, "end_line": 3602, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "<pre><code>aaa\n</code></pre>\n<p>bbb</p>\n", "example": 225, "start_line": 3605, "end_line": 3612, "section": "Paragraphs" }, { "markdown": "aaa \nbbb \n", "html": "<p>aaa<br />\nbbb</p>\n", "example": 226, "start_line": 3619, "end_line": 3625, "section": "Paragraphs" }, { "markdown": " \n\naaa\n \n\n# aaa\n\n \n", "html": "<p>aaa</p>\n<h1>aaa</h1>\n", "example": 227, "start_line": 3636, "end_line": 3648, "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": 228, "start_line": 3704, "end_line": 3714, "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": 229, "start_line": 3719, "end_line": 3729, "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": 230, "start_line": 3734, "end_line": 3744, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "<pre><code>> # Foo\n> bar\n> baz\n</code></pre>\n", "example": 231, "start_line": 3749, "end_line": 3758, "section": "Block quotes" }, { "markdown": "> # Foo\n> bar\nbaz\n", "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n", "example": 232, "start_line": 3764, "end_line": 3774, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n> foo\n", "html": "<blockquote>\n<p>bar\nbaz\nfoo</p>\n</blockquote>\n", "example": 233, "start_line": 3780, "end_line": 3790, "section": "Block quotes" }, { "markdown": "> foo\n---\n", "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n", "example": 234, "start_line": 3804, "end_line": 3812, "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": 235, "start_line": 3824, "end_line": 3836, "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": 236, "start_line": 3842, "end_line": 3852, "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": 237, "start_line": 3855, "end_line": 3865, "section": "Block quotes" }, { "markdown": "> foo\n - bar\n", "html": "<blockquote>\n<p>foo\n- bar</p>\n</blockquote>\n", "example": 238, "start_line": 3871, "end_line": 3879, "section": "Block quotes" }, { "markdown": ">\n", "html": "<blockquote>\n</blockquote>\n", "example": 239, "start_line": 3895, "end_line": 3900, "section": "Block quotes" }, { "markdown": ">\n> \n> \n", "html": "<blockquote>\n</blockquote>\n", "example": 240, "start_line": 3903, "end_line": 3910, "section": "Block quotes" }, { "markdown": ">\n> foo\n> \n", "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n", "example": 241, "start_line": 3915, "end_line": 3923, "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": 242, "start_line": 3928, "end_line": 3939, "section": "Block quotes" }, { "markdown": "> foo\n> bar\n", "html": "<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n", "example": 243, "start_line": 3950, "end_line": 3958, "section": "Block quotes" }, { "markdown": "> foo\n>\n> bar\n", "html": "<blockquote>\n<p>foo</p>\n<p>bar</p>\n</blockquote>\n", "example": 244, "start_line": 3963, "end_line": 3972, "section": "Block quotes" }, { "markdown": "foo\n> bar\n", "html": "<p>foo</p>\n<blockquote>\n<p>bar</p>\n</blockquote>\n", "example": 245, "start_line": 3977, "end_line": 3985, "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": 246, "start_line": 3991, "end_line": 4003, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n", "html": "<blockquote>\n<p>bar\nbaz</p>\n</blockquote>\n", "example": 247, "start_line": 4009, "end_line": 4017, "section": "Block quotes" }, { "markdown": "> bar\n\nbaz\n", "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n", "example": 248, "start_line": 4020, "end_line": 4029, "section": "Block quotes" }, { "markdown": "> bar\n>\nbaz\n", "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n", "example": 249, "start_line": 4032, "end_line": 4041, "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": 250, "start_line": 4048, "end_line": 4060, "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": 251, "start_line": 4063, "end_line": 4077, "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": 252, "start_line": 4085, "end_line": 4097, "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": 253, "start_line": 4139, "end_line": 4154, "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": 254, "start_line": 4161, "end_line": 4180, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "<ul>\n<li>one</li>\n</ul>\n<p>two</p>\n", "example": 255, "start_line": 4194, "end_line": 4203, "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": 256, "start_line": 4206, "end_line": 4217, "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": 257, "start_line": 4220, "end_line": 4230, "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": 258, "start_line": 4233, "end_line": 4244, "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": 259, "start_line": 4255, "end_line": 4270, "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": 260, "start_line": 4282, "end_line": 4295, "section": "List items" }, { "markdown": "-one\n\n2.two\n", "html": "<p>-one</p>\n<p>2.two</p>\n", "example": 261, "start_line": 4301, "end_line": 4308, "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": 262, "start_line": 4314, "end_line": 4326, "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": 263, "start_line": 4331, "end_line": 4353, "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": 264, "start_line": 4359, "end_line": 4377, "section": "List items" }, { "markdown": "123456789. ok\n", "html": "<ol start=\"123456789\">\n<li>ok</li>\n</ol>\n", "example": 265, "start_line": 4381, "end_line": 4387, "section": "List items" }, { "markdown": "1234567890. not ok\n", "html": "<p>1234567890. not ok</p>\n", "example": 266, "start_line": 4390, "end_line": 4394, "section": "List items" }, { "markdown": "0. ok\n", "html": "<ol start=\"0\">\n<li>ok</li>\n</ol>\n", "example": 267, "start_line": 4399, "end_line": 4405, "section": "List items" }, { "markdown": "003. ok\n", "html": "<ol start=\"3\">\n<li>ok</li>\n</ol>\n", "example": 268, "start_line": 4408, "end_line": 4414, "section": "List items" }, { "markdown": "-1. not ok\n", "html": "<p>-1. not ok</p>\n", "example": 269, "start_line": 4419, "end_line": 4423, "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": 270, "start_line": 4442, "end_line": 4454, "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": 271, "start_line": 4459, "end_line": 4471, "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": 272, "start_line": 4478, "end_line": 4490, "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": 273, "start_line": 4493, "end_line": 4509, "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": 274, "start_line": 4515, "end_line": 4531, "section": "List items" }, { "markdown": " foo\n\nbar\n", "html": "<p>foo</p>\n<p>bar</p>\n", "example": 275, "start_line": 4542, "end_line": 4549, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "<ul>\n<li>foo</li>\n</ul>\n<p>bar</p>\n", "example": 276, "start_line": 4552, "end_line": 4561, "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": 277, "start_line": 4569, "end_line": 4580, "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": 278, "start_line": 4596, "end_line": 4617, "section": "List items" }, { "markdown": "- \n foo\n", "html": "<ul>\n<li>foo</li>\n</ul>\n", "example": 279, "start_line": 4622, "end_line": 4629, "section": "List items" }, { "markdown": "-\n\n foo\n", "html": "<ul>\n<li></li>\n</ul>\n<p>foo</p>\n", "example": 280, "start_line": 4636, "end_line": 4645, "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": 281, "start_line": 4650, "end_line": 4660, "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": 282, "start_line": 4665, "end_line": 4675, "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": 283, "start_line": 4680, "end_line": 4690, "section": "List items" }, { "markdown": "*\n", "html": "<ul>\n<li></li>\n</ul>\n", "example": 284, "start_line": 4695, "end_line": 4701, "section": "List items" }, { "markdown": "foo\n*\n\nfoo\n1.\n", "html": "<p>foo\n*</p>\n<p>foo\n1.</p>\n", "example": 285, "start_line": 4705, "end_line": 4716, "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": 286, "start_line": 4727, "end_line": 4746, "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": 287, "start_line": 4751, "end_line": 4770, "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": 288, "start_line": 4775, "end_line": 4794, "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": 289, "start_line": 4799, "end_line": 4814, "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": 290, "start_line": 4829, "end_line": 4848, "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": 291, "start_line": 4853, "end_line": 4861, "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": 292, "start_line": 4866, "end_line": 4880, "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": 293, "start_line": 4883, "end_line": 4897, "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": 294, "start_line": 4911, "end_line": 4932, "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": 295, "start_line": 4937, "end_line": 4949, "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": 296, "start_line": 4954, "end_line": 4965, "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": 297, "start_line": 4970, "end_line": 4980, "section": "List items" }, { "markdown": "- - foo\n", "html": "<ul>\n<li>\n<ul>\n<li>foo</li>\n</ul>\n</li>\n</ul>\n", "example": 298, "start_line": 4985, "end_line": 4995, "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": 299, "start_line": 4998, "end_line": 5012, "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": 300, "start_line": 5017, "end_line": 5031, "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": 301, "start_line": 5253, "end_line": 5265, "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": 302, "start_line": 5268, "end_line": 5280, "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": 303, "start_line": 5287, "end_line": 5297, "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": 304, "start_line": 5364, "end_line": 5370, "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": 305, "start_line": 5374, "end_line": 5382, "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": 306, "start_line": 5388, "end_line": 5407, "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": 307, "start_line": 5409, "end_line": 5431, "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": 308, "start_line": 5439, "end_line": 5457, "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": 309, "start_line": 5460, "end_line": 5483, "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": 310, "start_line": 5491, "end_line": 5509, "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": 311, "start_line": 5512, "end_line": 5530, "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": 312, "start_line": 5536, "end_line": 5550, "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": 313, "start_line": 5556, "end_line": 5573, "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": 314, "start_line": 5579, "end_line": 5596, "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": 315, "start_line": 5601, "end_line": 5616, "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": 316, "start_line": 5623, "end_line": 5642, "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": 317, "start_line": 5645, "end_line": 5663, "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": 318, "start_line": 5668, "end_line": 5687, "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": 319, "start_line": 5694, "end_line": 5712, "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": 320, "start_line": 5718, "end_line": 5732, "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": 321, "start_line": 5738, "end_line": 5756, "section": "Lists" }, { "markdown": "- a\n", "html": "<ul>\n<li>a</li>\n</ul>\n", "example": 322, "start_line": 5761, "end_line": 5767, "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": 323, "start_line": 5770, "end_line": 5781, "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": 324, "start_line": 5787, "end_line": 5801, "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": 325, "start_line": 5806, "end_line": 5821, "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": 326, "start_line": 5824, "end_line": 5849, "section": "Lists" }, { "markdown": "`hi`lo`\n", "html": "<p><code>hi</code>lo`</p>\n", "example": 327, "start_line": 5858, "end_line": 5862, "section": "Inlines" }, { "markdown": "`foo`\n", "html": "<p><code>foo</code></p>\n", "example": 328, "start_line": 5890, "end_line": 5894, "section": "Code spans" }, { "markdown": "`` foo ` bar ``\n", "html": "<p><code>foo ` bar</code></p>\n", "example": 329, "start_line": 5901, "end_line": 5905, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "<p><code>``</code></p>\n", "example": 330, "start_line": 5911, "end_line": 5915, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "<p><code> `` </code></p>\n", "example": 331, "start_line": 5919, "end_line": 5923, "section": "Code spans" }, { "markdown": "` a`\n", "html": "<p><code> a</code></p>\n", "example": 332, "start_line": 5928, "end_line": 5932, "section": "Code spans" }, { "markdown": "` b `\n", "html": "<p><code> b </code></p>\n", "example": 333, "start_line": 5937, "end_line": 5941, "section": "Code spans" }, { "markdown": "` `\n` `\n", "html": "<p><code> </code>\n<code> </code></p>\n", "example": 334, "start_line": 5945, "end_line": 5951, "section": "Code spans" }, { "markdown": "``\nfoo\nbar \nbaz\n``\n", "html": "<p><code>foo bar baz</code></p>\n", "example": 335, "start_line": 5956, "end_line": 5964, "section": "Code spans" }, { "markdown": "``\nfoo \n``\n", "html": "<p><code>foo </code></p>\n", "example": 336, "start_line": 5966, "end_line": 5972, "section": "Code spans" }, { "markdown": "`foo bar \nbaz`\n", "html": "<p><code>foo bar baz</code></p>\n", "example": 337, "start_line": 5977, "end_line": 5982, "section": "Code spans" }, { "markdown": "`foo\\`bar`\n", "html": "<p><code>foo\\</code>bar`</p>\n", "example": 338, "start_line": 5994, "end_line": 5998, "section": "Code spans" }, { "markdown": "``foo`bar``\n", "html": "<p><code>foo`bar</code></p>\n", "example": 339, "start_line": 6005, "end_line": 6009, "section": "Code spans" }, { "markdown": "` foo `` bar `\n", "html": "<p><code>foo `` bar</code></p>\n", "example": 340, "start_line": 6011, "end_line": 6015, "section": "Code spans" }, { "markdown": "*foo`*`\n", "html": "<p>*foo<code>*</code></p>\n", "example": 341, "start_line": 6023, "end_line": 6027, "section": "Code spans" }, { "markdown": "[not a `link](/foo`)\n", "html": "<p>[not a <code>link](/foo</code>)</p>\n", "example": 342, "start_line": 6032, "end_line": 6036, "section": "Code spans" }, { "markdown": "`<a href=\"`\">`\n", "html": "<p><code><a href="</code>">`</p>\n", "example": 343, "start_line": 6042, "end_line": 6046, "section": "Code spans" }, { "markdown": "<a href=\"`\">`\n", "html": "<p><a href=\"`\">`</p>\n", "example": 344, "start_line": 6051, "end_line": 6055, "section": "Code spans" }, { "markdown": "`<http://foo.bar.`baz>`\n", "html": "<p><code><http://foo.bar.</code>baz>`</p>\n", "example": 345, "start_line": 6060, "end_line": 6064, "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": 6069, "end_line": 6073, "section": "Code spans" }, { "markdown": "```foo``\n", "html": "<p>```foo``</p>\n", "example": 347, "start_line": 6079, "end_line": 6083, "section": "Code spans" }, { "markdown": "`foo\n", "html": "<p>`foo</p>\n", "example": 348, "start_line": 6086, "end_line": 6090, "section": "Code spans" }, { "markdown": "`foo``bar``\n", "html": "<p>`foo<code>bar</code></p>\n", "example": 349, "start_line": 6095, "end_line": 6099, "section": "Code spans" }, { "markdown": "*foo bar*\n", "html": "<p><em>foo bar</em></p>\n", "example": 350, "start_line": 6312, "end_line": 6316, "section": "Emphasis and strong emphasis" }, { "markdown": "a * foo bar*\n", "html": "<p>a * foo bar*</p>\n", "example": 351, "start_line": 6322, "end_line": 6326, "section": "Emphasis and strong emphasis" }, { "markdown": "a*\"foo\"*\n", "html": "<p>a*"foo"*</p>\n", "example": 352, "start_line": 6333, "end_line": 6337, "section": "Emphasis and strong emphasis" }, { "markdown": "* a *\n", "html": "<p>* a *</p>\n", "example": 353, "start_line": 6342, "end_line": 6346, "section": "Emphasis and strong emphasis" }, { "markdown": "foo*bar*\n", "html": "<p>foo<em>bar</em></p>\n", "example": 354, "start_line": 6351, "end_line": 6355, "section": "Emphasis and strong emphasis" }, { "markdown": "5*6*78\n", "html": "<p>5<em>6</em>78</p>\n", "example": 355, "start_line": 6358, "end_line": 6362, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar_\n", "html": "<p><em>foo bar</em></p>\n", "example": 356, "start_line": 6367, "end_line": 6371, "section": "Emphasis and strong emphasis" }, { "markdown": "_ foo bar_\n", "html": "<p>_ foo bar_</p>\n", "example": 357, "start_line": 6377, "end_line": 6381, "section": "Emphasis and strong emphasis" }, { "markdown": "a_\"foo\"_\n", "html": "<p>a_"foo"_</p>\n", "example": 358, "start_line": 6387, "end_line": 6391, "section": "Emphasis and strong emphasis" }, { "markdown": "foo_bar_\n", "html": "<p>foo_bar_</p>\n", "example": 359, "start_line": 6396, "end_line": 6400, "section": "Emphasis and strong emphasis" }, { "markdown": "5_6_78\n", "html": "<p>5_6_78</p>\n", "example": 360, "start_line": 6403, "end_line": 6407, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням_стремятся_\n", "html": "<p>пристаням_стремятся_</p>\n", "example": 361, "start_line": 6410, "end_line": 6414, "section": "Emphasis and strong emphasis" }, { "markdown": "aa_\"bb\"_cc\n", "html": "<p>aa_"bb"_cc</p>\n", "example": 362, "start_line": 6420, "end_line": 6424, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-_(bar)_\n", "html": "<p>foo-<em>(bar)</em></p>\n", "example": 363, "start_line": 6431, "end_line": 6435, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo*\n", "html": "<p>_foo*</p>\n", "example": 364, "start_line": 6443, "end_line": 6447, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar *\n", "html": "<p>*foo bar *</p>\n", "example": 365, "start_line": 6453, "end_line": 6457, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar\n*\n", "html": "<p>*foo bar\n*</p>\n", "example": 366, "start_line": 6462, "end_line": 6468, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo)\n", "html": "<p>*(*foo)</p>\n", "example": 367, "start_line": 6475, "end_line": 6479, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo*)*\n", "html": "<p><em>(<em>foo</em>)</em></p>\n", "example": 368, "start_line": 6485, "end_line": 6489, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo*bar\n", "html": "<p><em>foo</em>bar</p>\n", "example": 369, "start_line": 6494, "end_line": 6498, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar _\n", "html": "<p>_foo bar _</p>\n", "example": 370, "start_line": 6507, "end_line": 6511, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo)\n", "html": "<p>_(_foo)</p>\n", "example": 371, "start_line": 6517, "end_line": 6521, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo_)_\n", "html": "<p><em>(<em>foo</em>)</em></p>\n", "example": 372, "start_line": 6526, "end_line": 6530, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar\n", "html": "<p>_foo_bar</p>\n", "example": 373, "start_line": 6535, "end_line": 6539, "section": "Emphasis and strong emphasis" }, { "markdown": "_пристаням_стремятся\n", "html": "<p>_пристаням_стремятся</p>\n", "example": 374, "start_line": 6542, "end_line": 6546, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar_baz_\n", "html": "<p><em>foo_bar_baz</em></p>\n", "example": 375, "start_line": 6549, "end_line": 6553, "section": "Emphasis and strong emphasis" }, { "markdown": "_(bar)_.\n", "html": "<p><em>(bar)</em>.</p>\n", "example": 376, "start_line": 6560, "end_line": 6564, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar**\n", "html": "<p><strong>foo bar</strong></p>\n", "example": 377, "start_line": 6569, "end_line": 6573, "section": "Emphasis and strong emphasis" }, { "markdown": "** foo bar**\n", "html": "<p>** foo bar**</p>\n", "example": 378, "start_line": 6579, "end_line": 6583, "section": "Emphasis and strong emphasis" }, { "markdown": "a**\"foo\"**\n", "html": "<p>a**"foo"**</p>\n", "example": 379, "start_line": 6590, "end_line": 6594, "section": "Emphasis and strong emphasis" }, { "markdown": "foo**bar**\n", "html": "<p>foo<strong>bar</strong></p>\n", "example": 380, "start_line": 6599, "end_line": 6603, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar__\n", "html": "<p><strong>foo bar</strong></p>\n", "example": 381, "start_line": 6608, "end_line": 6612, "section": "Emphasis and strong emphasis" }, { "markdown": "__ foo bar__\n", "html": "<p>__ foo bar__</p>\n", "example": 382, "start_line": 6618, "end_line": 6622, "section": "Emphasis and strong emphasis" }, { "markdown": "__\nfoo bar__\n", "html": "<p>__\nfoo bar__</p>\n", "example": 383, "start_line": 6626, "end_line": 6632, "section": "Emphasis and strong emphasis" }, { "markdown": "a__\"foo\"__\n", "html": "<p>a__"foo"__</p>\n", "example": 384, "start_line": 6638, "end_line": 6642, "section": "Emphasis and strong emphasis" }, { "markdown": "foo__bar__\n", "html": "<p>foo__bar__</p>\n", "example": 385, "start_line": 6647, "end_line": 6651, "section": "Emphasis and strong emphasis" }, { "markdown": "5__6__78\n", "html": "<p>5__6__78</p>\n", "example": 386, "start_line": 6654, "end_line": 6658, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням__стремятся__\n", "html": "<p>пристаням__стремятся__</p>\n", "example": 387, "start_line": 6661, "end_line": 6665, "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": 6668, "end_line": 6672, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-__(bar)__\n", "html": "<p>foo-<strong>(bar)</strong></p>\n", "example": 389, "start_line": 6679, "end_line": 6683, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar **\n", "html": "<p>**foo bar **</p>\n", "example": 390, "start_line": 6692, "end_line": 6696, "section": "Emphasis and strong emphasis" }, { "markdown": "**(**foo)\n", "html": "<p>**(**foo)</p>\n", "example": 391, "start_line": 6705, "end_line": 6709, "section": "Emphasis and strong emphasis" }, { "markdown": "*(**foo**)*\n", "html": "<p><em>(<strong>foo</strong>)</em></p>\n", "example": 392, "start_line": 6715, "end_line": 6719, "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": 6722, "end_line": 6728, "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": 6731, "end_line": 6735, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**bar\n", "html": "<p><strong>foo</strong>bar</p>\n", "example": 395, "start_line": 6740, "end_line": 6744, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar __\n", "html": "<p>__foo bar __</p>\n", "example": 396, "start_line": 6752, "end_line": 6756, "section": "Emphasis and strong emphasis" }, { "markdown": "__(__foo)\n", "html": "<p>__(__foo)</p>\n", "example": 397, "start_line": 6762, "end_line": 6766, "section": "Emphasis and strong emphasis" }, { "markdown": "_(__foo__)_\n", "html": "<p><em>(<strong>foo</strong>)</em></p>\n", "example": 398, "start_line": 6772, "end_line": 6776, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar\n", "html": "<p>__foo__bar</p>\n", "example": 399, "start_line": 6781, "end_line": 6785, "section": "Emphasis and strong emphasis" }, { "markdown": "__пристаням__стремятся\n", "html": "<p>__пристаням__стремятся</p>\n", "example": 400, "start_line": 6788, "end_line": 6792, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar__baz__\n", "html": "<p><strong>foo__bar__baz</strong></p>\n", "example": 401, "start_line": 6795, "end_line": 6799, "section": "Emphasis and strong emphasis" }, { "markdown": "__(bar)__.\n", "html": "<p><strong>(bar)</strong>.</p>\n", "example": 402, "start_line": 6806, "end_line": 6810, "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": 6818, "end_line": 6822, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo\nbar*\n", "html": "<p><em>foo\nbar</em></p>\n", "example": 404, "start_line": 6825, "end_line": 6831, "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": 6837, "end_line": 6841, "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": 6844, "end_line": 6848, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_ bar_\n", "html": "<p><em><em>foo</em> bar</em></p>\n", "example": 407, "start_line": 6851, "end_line": 6855, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar**\n", "html": "<p><em>foo <em>bar</em></em></p>\n", "example": 408, "start_line": 6858, "end_line": 6862, "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": 6865, "end_line": 6869, "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": 6871, "end_line": 6875, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar*\n", "html": "<p><em>foo**bar</em></p>\n", "example": 411, "start_line": 6895, "end_line": 6899, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo** bar*\n", "html": "<p><em><strong>foo</strong> bar</em></p>\n", "example": 412, "start_line": 6908, "end_line": 6912, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar***\n", "html": "<p><em>foo <strong>bar</strong></em></p>\n", "example": 413, "start_line": 6915, "end_line": 6919, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar***\n", "html": "<p><em>foo<strong>bar</strong></em></p>\n", "example": 414, "start_line": 6922, "end_line": 6926, "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": 6933, "end_line": 6937, "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": 6939, "end_line": 6943, "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": 6948, "end_line": 6952, "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": 6955, "end_line": 6959, "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": 6964, "end_line": 6968, "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": 6971, "end_line": 6975, "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": 6984, "end_line": 6988, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo\nbar**\n", "html": "<p><strong>foo\nbar</strong></p>\n", "example": 422, "start_line": 6991, "end_line": 6997, "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": 7003, "end_line": 7007, "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": 7010, "end_line": 7014, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo__ bar__\n", "html": "<p><strong><strong>foo</strong> bar</strong></p>\n", "example": 425, "start_line": 7017, "end_line": 7021, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar****\n", "html": "<p><strong>foo <strong>bar</strong></strong></p>\n", "example": 426, "start_line": 7024, "end_line": 7028, "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": 7031, "end_line": 7035, "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": 7038, "end_line": 7042, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo* bar**\n", "html": "<p><strong><em>foo</em> bar</strong></p>\n", "example": 429, "start_line": 7045, "end_line": 7049, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar***\n", "html": "<p><strong>foo <em>bar</em></strong></p>\n", "example": 430, "start_line": 7052, "end_line": 7056, "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": 7061, "end_line": 7067, "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": 7070, "end_line": 7074, "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": 7079, "end_line": 7083, "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": 7086, "end_line": 7090, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ***\n", "html": "<p>foo ***</p>\n", "example": 435, "start_line": 7096, "end_line": 7100, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *\\**\n", "html": "<p>foo <em>*</em></p>\n", "example": 436, "start_line": 7103, "end_line": 7107, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *_*\n", "html": "<p>foo <em>_</em></p>\n", "example": 437, "start_line": 7110, "end_line": 7114, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *****\n", "html": "<p>foo *****</p>\n", "example": 438, "start_line": 7117, "end_line": 7121, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **\\***\n", "html": "<p>foo <strong>*</strong></p>\n", "example": 439, "start_line": 7124, "end_line": 7128, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **_**\n", "html": "<p>foo <strong>_</strong></p>\n", "example": 440, "start_line": 7131, "end_line": 7135, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*\n", "html": "<p>*<em>foo</em></p>\n", "example": 441, "start_line": 7142, "end_line": 7146, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**\n", "html": "<p><em>foo</em>*</p>\n", "example": 442, "start_line": 7149, "end_line": 7153, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo**\n", "html": "<p>*<strong>foo</strong></p>\n", "example": 443, "start_line": 7156, "end_line": 7160, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo*\n", "html": "<p>***<em>foo</em></p>\n", "example": 444, "start_line": 7163, "end_line": 7167, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo***\n", "html": "<p><strong>foo</strong>*</p>\n", "example": 445, "start_line": 7170, "end_line": 7174, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo****\n", "html": "<p><em>foo</em>***</p>\n", "example": 446, "start_line": 7177, "end_line": 7181, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ___\n", "html": "<p>foo ___</p>\n", "example": 447, "start_line": 7187, "end_line": 7191, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _\\__\n", "html": "<p>foo <em>_</em></p>\n", "example": 448, "start_line": 7194, "end_line": 7198, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _*_\n", "html": "<p>foo <em>*</em></p>\n", "example": 449, "start_line": 7201, "end_line": 7205, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _____\n", "html": "<p>foo _____</p>\n", "example": 450, "start_line": 7208, "end_line": 7212, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __\\___\n", "html": "<p>foo <strong>_</strong></p>\n", "example": 451, "start_line": 7215, "end_line": 7219, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __*__\n", "html": "<p>foo <strong>*</strong></p>\n", "example": 452, "start_line": 7222, "end_line": 7226, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_\n", "html": "<p>_<em>foo</em></p>\n", "example": 453, "start_line": 7229, "end_line": 7233, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo__\n", "html": "<p><em>foo</em>_</p>\n", "example": 454, "start_line": 7240, "end_line": 7244, "section": "Emphasis and strong emphasis" }, { "markdown": "___foo__\n", "html": "<p>_<strong>foo</strong></p>\n", "example": 455, "start_line": 7247, "end_line": 7251, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo_\n", "html": "<p>___<em>foo</em></p>\n", "example": 456, "start_line": 7254, "end_line": 7258, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo___\n", "html": "<p><strong>foo</strong>_</p>\n", "example": 457, "start_line": 7261, "end_line": 7265, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo____\n", "html": "<p><em>foo</em>___</p>\n", "example": 458, "start_line": 7268, "end_line": 7272, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**\n", "html": "<p><strong>foo</strong></p>\n", "example": 459, "start_line": 7278, "end_line": 7282, "section": "Emphasis and strong emphasis" }, { "markdown": "*_foo_*\n", "html": "<p><em><em>foo</em></em></p>\n", "example": 460, "start_line": 7285, "end_line": 7289, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__\n", "html": "<p><strong>foo</strong></p>\n", "example": 461, "start_line": 7292, "end_line": 7296, "section": "Emphasis and strong emphasis" }, { "markdown": "_*foo*_\n", "html": "<p><em><em>foo</em></em></p>\n", "example": 462, "start_line": 7299, "end_line": 7303, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo****\n", "html": "<p><strong><strong>foo</strong></strong></p>\n", "example": 463, "start_line": 7309, "end_line": 7313, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo____\n", "html": "<p><strong><strong>foo</strong></strong></p>\n", "example": 464, "start_line": 7316, "end_line": 7320, "section": "Emphasis and strong emphasis" }, { "markdown": "******foo******\n", "html": "<p><strong><strong><strong>foo</strong></strong></strong></p>\n", "example": 465, "start_line": 7327, "end_line": 7331, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo***\n", "html": "<p><em><strong>foo</strong></em></p>\n", "example": 466, "start_line": 7336, "end_line": 7340, "section": "Emphasis and strong emphasis" }, { "markdown": "_____foo_____\n", "html": "<p><em><strong><strong>foo</strong></strong></em></p>\n", "example": 467, "start_line": 7343, "end_line": 7347, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo _bar* baz_\n", "html": "<p><em>foo _bar</em> baz_</p>\n", "example": 468, "start_line": 7352, "end_line": 7356, "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": 7359, "end_line": 7363, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar baz**\n", "html": "<p>**foo <strong>bar baz</strong></p>\n", "example": 470, "start_line": 7368, "end_line": 7372, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar baz*\n", "html": "<p>*foo <em>bar baz</em></p>\n", "example": 471, "start_line": 7375, "end_line": 7379, "section": "Emphasis and strong emphasis" }, { "markdown": "*[bar*](/url)\n", "html": "<p>*<a href=\"/url\">bar*</a></p>\n", "example": 472, "start_line": 7384, "end_line": 7388, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo [bar_](/url)\n", "html": "<p>_foo <a href=\"/url\">bar_</a></p>\n", "example": 473, "start_line": 7391, "end_line": 7395, "section": "Emphasis and strong emphasis" }, { "markdown": "*<img src=\"foo\" title=\"*\"/>\n", "html": "<p>*<img src=\"foo\" title=\"*\"/></p>\n", "example": 474, "start_line": 7398, "end_line": 7402, "section": "Emphasis and strong emphasis" }, { "markdown": "**<a href=\"**\">\n", "html": "<p>**<a href=\"**\"></p>\n", "example": 475, "start_line": 7405, "end_line": 7409, "section": "Emphasis and strong emphasis" }, { "markdown": "__<a href=\"__\">\n", "html": "<p>__<a href=\"__\"></p>\n", "example": 476, "start_line": 7412, "end_line": 7416, "section": "Emphasis and strong emphasis" }, { "markdown": "*a `*`*\n", "html": "<p><em>a <code>*</code></em></p>\n", "example": 477, "start_line": 7419, "end_line": 7423, "section": "Emphasis and strong emphasis" }, { "markdown": "_a `_`_\n", "html": "<p><em>a <code>_</code></em></p>\n", "example": 478, "start_line": 7426, "end_line": 7430, "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": 7433, "end_line": 7437, "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": 7440, "end_line": 7444, "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": 7528, "end_line": 7532, "section": "Links" }, { "markdown": "[link](/uri)\n", "html": "<p><a href=\"/uri\">link</a></p>\n", "example": 482, "start_line": 7538, "end_line": 7542, "section": "Links" }, { "markdown": "[](./target.md)\n", "html": "<p><a href=\"./target.md\"></a></p>\n", "example": 483, "start_line": 7544, "end_line": 7548, "section": "Links" }, { "markdown": "[link]()\n", "html": "<p><a href=\"\">link</a></p>\n", "example": 484, "start_line": 7551, "end_line": 7555, "section": "Links" }, { "markdown": "[link](<>)\n", "html": "<p><a href=\"\">link</a></p>\n", "example": 485, "start_line": 7558, "end_line": 7562, "section": "Links" }, { "markdown": "[]()\n", "html": "<p><a href=\"\"></a></p>\n", "example": 486, "start_line": 7565, "end_line": 7569, "section": "Links" }, { "markdown": "[link](/my uri)\n", "html": "<p>[link](/my uri)</p>\n", "example": 487, "start_line": 7574, "end_line": 7578, "section": "Links" }, { "markdown": "[link](</my uri>)\n", "html": "<p><a href=\"/my%20uri\">link</a></p>\n", "example": 488, "start_line": 7580, "end_line": 7584, "section": "Links" }, { "markdown": "[link](foo\nbar)\n", "html": "<p>[link](foo\nbar)</p>\n", "example": 489, "start_line": 7589, "end_line": 7595, "section": "Links" }, { "markdown": "[link](<foo\nbar>)\n", "html": "<p>[link](<foo\nbar>)</p>\n", "example": 490, "start_line": 7597, "end_line": 7603, "section": "Links" }, { "markdown": "[a](<b)c>)\n", "html": "<p><a href=\"b)c\">a</a></p>\n", "example": 491, "start_line": 7608, "end_line": 7612, "section": "Links" }, { "markdown": "[link](<foo\\>)\n", "html": "<p>[link](<foo>)</p>\n", "example": 492, "start_line": 7616, "end_line": 7620, "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": 493, "start_line": 7625, "end_line": 7633, "section": "Links" }, { "markdown": "[link](\\(foo\\))\n", "html": "<p><a href=\"(foo)\">link</a></p>\n", "example": 494, "start_line": 7637, "end_line": 7641, "section": "Links" }, { "markdown": "[link](foo(and(bar)))\n", "html": "<p><a href=\"foo(and(bar))\">link</a></p>\n", "example": 495, "start_line": 7646, "end_line": 7650, "section": "Links" }, { "markdown": "[link](foo(and(bar))\n", "html": "<p>[link](foo(and(bar))</p>\n", "example": 496, "start_line": 7655, "end_line": 7659, "section": "Links" }, { "markdown": "[link](foo\\(and\\(bar\\))\n", "html": "<p><a href=\"foo(and(bar)\">link</a></p>\n", "example": 497, "start_line": 7662, "end_line": 7666, "section": "Links" }, { "markdown": "[link](<foo(and(bar)>)\n", "html": "<p><a href=\"foo(and(bar)\">link</a></p>\n", "example": 498, "start_line": 7669, "end_line": 7673, "section": "Links" }, { "markdown": "[link](foo\\)\\:)\n", "html": "<p><a href=\"foo):\">link</a></p>\n", "example": 499, "start_line": 7679, "end_line": 7683, "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": 500, "start_line": 7688, "end_line": 7698, "section": "Links" }, { "markdown": "[link](foo\\bar)\n", "html": "<p><a href=\"foo%5Cbar\">link</a></p>\n", "example": 501, "start_line": 7704, "end_line": 7708, "section": "Links" }, { "markdown": "[link](foo%20bä)\n", "html": "<p><a href=\"foo%20b%C3%A4\">link</a></p>\n", "example": 502, "start_line": 7720, "end_line": 7724, "section": "Links" }, { "markdown": "[link](\"title\")\n", "html": "<p><a href=\"%22title%22\">link</a></p>\n", "example": 503, "start_line": 7731, "end_line": 7735, "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": 504, "start_line": 7740, "end_line": 7748, "section": "Links" }, { "markdown": "[link](/url \"title \\\""\")\n", "html": "<p><a href=\"/url\" title=\"title ""\">link</a></p>\n", "example": 505, "start_line": 7754, "end_line": 7758, "section": "Links" }, { "markdown": "[link](/url \"title\")\n", "html": "<p><a href=\"/url%C2%A0%22title%22\">link</a></p>\n", "example": 506, "start_line": 7765, "end_line": 7769, "section": "Links" }, { "markdown": "[link](/url \"title \"and\" title\")\n", "html": "<p>[link](/url "title "and" title")</p>\n", "example": 507, "start_line": 7774, "end_line": 7778, "section": "Links" }, { "markdown": "[link](/url 'title \"and\" title')\n", "html": "<p><a href=\"/url\" title=\"title "and" title\">link</a></p>\n", "example": 508, "start_line": 7783, "end_line": 7787, "section": "Links" }, { "markdown": "[link]( /uri\n \"title\" )\n", "html": "<p><a href=\"/uri\" title=\"title\">link</a></p>\n", "example": 509, "start_line": 7808, "end_line": 7813, "section": "Links" }, { "markdown": "[link] (/uri)\n", "html": "<p>[link] (/uri)</p>\n", "example": 510, "start_line": 7819, "end_line": 7823, "section": "Links" }, { "markdown": "[link [foo [bar]]](/uri)\n", "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n", "example": 511, "start_line": 7829, "end_line": 7833, "section": "Links" }, { "markdown": "[link] bar](/uri)\n", "html": "<p>[link] bar](/uri)</p>\n", "example": 512, "start_line": 7836, "end_line": 7840, "section": "Links" }, { "markdown": "[link [bar](/uri)\n", "html": "<p>[link <a href=\"/uri\">bar</a></p>\n", "example": 513, "start_line": 7843, "end_line": 7847, "section": "Links" }, { "markdown": "[link \\[bar](/uri)\n", "html": "<p><a href=\"/uri\">link [bar</a></p>\n", "example": 514, "start_line": 7850, "end_line": 7854, "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": 515, "start_line": 7859, "end_line": 7863, "section": "Links" }, { "markdown": "[![moon](moon.jpg)](/uri)\n", "html": "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\" /></a></p>\n", "example": 516, "start_line": 7866, "end_line": 7870, "section": "Links" }, { "markdown": "[foo [bar](/uri)](/uri)\n", "html": "<p>[foo <a href=\"/uri\">bar</a>](/uri)</p>\n", "example": 517, "start_line": 7875, "end_line": 7879, "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": 518, "start_line": 7882, "end_line": 7886, "section": "Links" }, { "markdown": "![[[foo](uri1)](uri2)](uri3)\n", "html": "<p><img src=\"uri3\" alt=\"[foo](uri2)\" /></p>\n", "example": 519, "start_line": 7889, "end_line": 7893, "section": "Links" }, { "markdown": "*[foo*](/uri)\n", "html": "<p>*<a href=\"/uri\">foo*</a></p>\n", "example": 520, "start_line": 7899, "end_line": 7903, "section": "Links" }, { "markdown": "[foo *bar](baz*)\n", "html": "<p><a href=\"baz*\">foo *bar</a></p>\n", "example": 521, "start_line": 7906, "end_line": 7910, "section": "Links" }, { "markdown": "*foo [bar* baz]\n", "html": "<p><em>foo [bar</em> baz]</p>\n", "example": 522, "start_line": 7916, "end_line": 7920, "section": "Links" }, { "markdown": "[foo <bar attr=\"](baz)\">\n", "html": "<p>[foo <bar attr=\"](baz)\"></p>\n", "example": 523, "start_line": 7926, "end_line": 7930, "section": "Links" }, { "markdown": "[foo`](/uri)`\n", "html": "<p>[foo<code>](/uri)</code></p>\n", "example": 524, "start_line": 7933, "end_line": 7937, "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": 525, "start_line": 7940, "end_line": 7944, "section": "Links" }, { "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 526, "start_line": 7978, "end_line": 7984, "section": "Links" }, { "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n", "example": 527, "start_line": 7993, "end_line": 7999, "section": "Links" }, { "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\">link [bar</a></p>\n", "example": 528, "start_line": 8002, "end_line": 8008, "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": 529, "start_line": 8013, "end_line": 8019, "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": 530, "start_line": 8022, "end_line": 8028, "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": 531, "start_line": 8033, "end_line": 8039, "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": 532, "start_line": 8042, "end_line": 8048, "section": "Links" }, { "markdown": "*[foo*][ref]\n\n[ref]: /uri\n", "html": "<p>*<a href=\"/uri\">foo*</a></p>\n", "example": 533, "start_line": 8057, "end_line": 8063, "section": "Links" }, { "markdown": "[foo *bar][ref]*\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\">foo *bar</a>*</p>\n", "example": 534, "start_line": 8066, "end_line": 8072, "section": "Links" }, { "markdown": "[foo <bar attr=\"][ref]\">\n\n[ref]: /uri\n", "html": "<p>[foo <bar attr=\"][ref]\"></p>\n", "example": 535, "start_line": 8078, "end_line": 8084, "section": "Links" }, { "markdown": "[foo`][ref]`\n\n[ref]: /uri\n", "html": "<p>[foo<code>][ref]</code></p>\n", "example": 536, "start_line": 8087, "end_line": 8093, "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": 537, "start_line": 8096, "end_line": 8102, "section": "Links" }, { "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 538, "start_line": 8107, "end_line": 8113, "section": "Links" }, { "markdown": "[ẞ]\n\n[SS]: /url\n", "html": "<p><a href=\"/url\">ẞ</a></p>\n", "example": 539, "start_line": 8118, "end_line": 8124, "section": "Links" }, { "markdown": "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n", "html": "<p><a href=\"/url\">Baz</a></p>\n", "example": 540, "start_line": 8130, "end_line": 8137, "section": "Links" }, { "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n", "html": "<p>[foo] <a href=\"/url\" title=\"title\">bar</a></p>\n", "example": 541, "start_line": 8143, "end_line": 8149, "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": 542, "start_line": 8152, "end_line": 8160, "section": "Links" }, { "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n", "html": "<p><a href=\"/url1\">bar</a></p>\n", "example": 543, "start_line": 8193, "end_line": 8201, "section": "Links" }, { "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n", "html": "<p>[bar][foo!]</p>\n", "example": 544, "start_line": 8208, "end_line": 8214, "section": "Links" }, { "markdown": "[foo][ref[]\n\n[ref[]: /uri\n", "html": "<p>[foo][ref[]</p>\n<p>[ref[]: /uri</p>\n", "example": 545, "start_line": 8220, "end_line": 8227, "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": 546, "start_line": 8230, "end_line": 8237, "section": "Links" }, { "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n", "html": "<p>[[[foo]]]</p>\n<p>[[[foo]]]: /url</p>\n", "example": 547, "start_line": 8240, "end_line": 8247, "section": "Links" }, { "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n", "html": "<p><a href=\"/uri\">foo</a></p>\n", "example": 548, "start_line": 8250, "end_line": 8256, "section": "Links" }, { "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n", "html": "<p><a href=\"/uri\">bar\\</a></p>\n", "example": 549, "start_line": 8261, "end_line": 8267, "section": "Links" }, { "markdown": "[]\n\n[]: /uri\n", "html": "<p>[]</p>\n<p>[]: /uri</p>\n", "example": 550, "start_line": 8273, "end_line": 8280, "section": "Links" }, { "markdown": "[\n ]\n\n[\n ]: /uri\n", "html": "<p>[\n]</p>\n<p>[\n]: /uri</p>\n", "example": 551, "start_line": 8283, "end_line": 8294, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 552, "start_line": 8306, "end_line": 8312, "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": 553, "start_line": 8315, "end_line": 8321, "section": "Links" }, { "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n", "example": 554, "start_line": 8326, "end_line": 8332, "section": "Links" }, { "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a>\n[]</p>\n", "example": 555, "start_line": 8339, "end_line": 8347, "section": "Links" }, { "markdown": "[foo]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 556, "start_line": 8359, "end_line": 8365, "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": 557, "start_line": 8368, "end_line": 8374, "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": 558, "start_line": 8377, "end_line": 8383, "section": "Links" }, { "markdown": "[[bar [foo]\n\n[foo]: /url\n", "html": "<p>[[bar <a href=\"/url\">foo</a></p>\n", "example": 559, "start_line": 8386, "end_line": 8392, "section": "Links" }, { "markdown": "[Foo]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n", "example": 560, "start_line": 8397, "end_line": 8403, "section": "Links" }, { "markdown": "[foo] bar\n\n[foo]: /url\n", "html": "<p><a href=\"/url\">foo</a> bar</p>\n", "example": 561, "start_line": 8408, "end_line": 8414, "section": "Links" }, { "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n", "html": "<p>[foo]</p>\n", "example": 562, "start_line": 8420, "end_line": 8426, "section": "Links" }, { "markdown": "[foo*]: /url\n\n*[foo*]\n", "html": "<p>*<a href=\"/url\">foo*</a></p>\n", "example": 563, "start_line": 8432, "end_line": 8438, "section": "Links" }, { "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n", "html": "<p><a href=\"/url2\">foo</a></p>\n", "example": 564, "start_line": 8444, "end_line": 8451, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url1\n", "html": "<p><a href=\"/url1\">foo</a></p>\n", "example": 565, "start_line": 8453, "end_line": 8459, "section": "Links" }, { "markdown": "[foo]()\n\n[foo]: /url1\n", "html": "<p><a href=\"\">foo</a></p>\n", "example": 566, "start_line": 8463, "end_line": 8469, "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": 567, "start_line": 8471, "end_line": 8477, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url\n", "html": "<p>[foo]<a href=\"/url\">bar</a></p>\n", "example": 568, "start_line": 8482, "end_line": 8488, "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": 569, "start_line": 8494, "end_line": 8501, "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": 570, "start_line": 8507, "end_line": 8514, "section": "Links" }, { "markdown": "![foo](/url \"title\")\n", "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n", "example": 571, "start_line": 8530, "end_line": 8534, "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": 8537, "end_line": 8543, "section": "Images" }, { "markdown": "![foo ![bar](/url)](/url2)\n", "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n", "example": 573, "start_line": 8546, "end_line": 8550, "section": "Images" }, { "markdown": "![foo [bar](/url)](/url2)\n", "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n", "example": 574, "start_line": 8553, "end_line": 8557, "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": 575, "start_line": 8567, "end_line": 8573, "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": 576, "start_line": 8576, "end_line": 8582, "section": "Images" }, { "markdown": "![foo](train.jpg)\n", "html": "<p><img src=\"train.jpg\" alt=\"foo\" /></p>\n", "example": 577, "start_line": 8585, "end_line": 8589, "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": 578, "start_line": 8592, "end_line": 8596, "section": "Images" }, { "markdown": "![foo](<url>)\n", "html": "<p><img src=\"url\" alt=\"foo\" /></p>\n", "example": 579, "start_line": 8599, "end_line": 8603, "section": "Images" }, { "markdown": "![](/url)\n", "html": "<p><img src=\"/url\" alt=\"\" /></p>\n", "example": 580, "start_line": 8606, "end_line": 8610, "section": "Images" }, { "markdown": "![foo][bar]\n\n[bar]: /url\n", "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n", "example": 581, "start_line": 8615, "end_line": 8621, "section": "Images" }, { "markdown": "![foo][bar]\n\n[BAR]: /url\n", "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n", "example": 582, "start_line": 8624, "end_line": 8630, "section": "Images" }, { "markdown": "![foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n", "example": 583, "start_line": 8635, "end_line": 8641, "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": 584, "start_line": 8644, "end_line": 8650, "section": "Images" }, { "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n", "example": 585, "start_line": 8655, "end_line": 8661, "section": "Images" }, { "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" />\n[]</p>\n", "example": 586, "start_line": 8667, "end_line": 8675, "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": 8680, "end_line": 8686, "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": 588, "start_line": 8689, "end_line": 8695, "section": "Images" }, { "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n", "html": "<p>![[foo]]</p>\n<p>[[foo]]: /url "title"</p>\n", "example": 589, "start_line": 8700, "end_line": 8707, "section": "Images" }, { "markdown": "![Foo]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n", "example": 590, "start_line": 8712, "end_line": 8718, "section": "Images" }, { "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n", "html": "<p>![foo]</p>\n", "example": 591, "start_line": 8724, "end_line": 8730, "section": "Images" }, { "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n", "html": "<p>!<a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 592, "start_line": 8736, "end_line": 8742, "section": "Images" }, { "markdown": "<http://foo.bar.baz>\n", "html": "<p><a href=\"http://foo.bar.baz\">http://foo.bar.baz</a></p>\n", "example": 593, "start_line": 8769, "end_line": 8773, "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": 594, "start_line": 8776, "end_line": 8780, "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": 595, "start_line": 8783, "end_line": 8787, "section": "Autolinks" }, { "markdown": "<MAILTO:FOO@BAR.BAZ>\n", "html": "<p><a href=\"MAILTO:FOO@BAR.BAZ\">MAILTO:FOO@BAR.BAZ</a></p>\n", "example": 596, "start_line": 8792, "end_line": 8796, "section": "Autolinks" }, { "markdown": "<a+b+c:d>\n", "html": "<p><a href=\"a+b+c:d\">a+b+c:d</a></p>\n", "example": 597, "start_line": 8804, "end_line": 8808, "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": 598, "start_line": 8811, "end_line": 8815, "section": "Autolinks" }, { "markdown": "<http://../>\n", "html": "<p><a href=\"http://../\">http://../</a></p>\n", "example": 599, "start_line": 8818, "end_line": 8822, "section": "Autolinks" }, { "markdown": "<localhost:5001/foo>\n", "html": "<p><a href=\"localhost:5001/foo\">localhost:5001/foo</a></p>\n", "example": 600, "start_line": 8825, "end_line": 8829, "section": "Autolinks" }, { "markdown": "<http://foo.bar/baz bim>\n", "html": "<p><http://foo.bar/baz bim></p>\n", "example": 601, "start_line": 8834, "end_line": 8838, "section": "Autolinks" }, { "markdown": "<http://example.com/\\[\\>\n", "html": "<p><a href=\"http://example.com/%5C%5B%5C\">http://example.com/\\[\\</a></p>\n", "example": 602, "start_line": 8843, "end_line": 8847, "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": 603, "start_line": 8865, "end_line": 8869, "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": 604, "start_line": 8872, "end_line": 8876, "section": "Autolinks" }, { "markdown": "<foo\\+@bar.example.com>\n", "html": "<p><foo+@bar.example.com></p>\n", "example": 605, "start_line": 8881, "end_line": 8885, "section": "Autolinks" }, { "markdown": "<>\n", "html": "<p><></p>\n", "example": 606, "start_line": 8890, "end_line": 8894, "section": "Autolinks" }, { "markdown": "< http://foo.bar >\n", "html": "<p>< http://foo.bar ></p>\n", "example": 607, "start_line": 8897, "end_line": 8901, "section": "Autolinks" }, { "markdown": "<m:abc>\n", "html": "<p><m:abc></p>\n", "example": 608, "start_line": 8904, "end_line": 8908, "section": "Autolinks" }, { "markdown": "<foo.bar.baz>\n", "html": "<p><foo.bar.baz></p>\n", "example": 609, "start_line": 8911, "end_line": 8915, "section": "Autolinks" }, { "markdown": "http://example.com\n", "html": "<p>http://example.com</p>\n", "example": 610, "start_line": 8918, "end_line": 8922, "section": "Autolinks" }, { "markdown": "foo@bar.example.com\n", "html": "<p>foo@bar.example.com</p>\n", "example": 611, "start_line": 8925, "end_line": 8929, "section": "Autolinks" }, { "markdown": "<a><bab><c2c>\n", "html": "<p><a><bab><c2c></p>\n", "example": 612, "start_line": 9006, "end_line": 9010, "section": "Raw HTML" }, { "markdown": "<a/><b2/>\n", "html": "<p><a/><b2/></p>\n", "example": 613, "start_line": 9015, "end_line": 9019, "section": "Raw HTML" }, { "markdown": "<a /><b2\ndata=\"foo\" >\n", "html": "<p><a /><b2\ndata=\"foo\" ></p>\n", "example": 614, "start_line": 9024, "end_line": 9030, "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": 615, "start_line": 9035, "end_line": 9041, "section": "Raw HTML" }, { "markdown": "Foo <responsive-image src=\"foo.jpg\" />\n", "html": "<p>Foo <responsive-image src=\"foo.jpg\" /></p>\n", "example": 616, "start_line": 9046, "end_line": 9050, "section": "Raw HTML" }, { "markdown": "<33> <__>\n", "html": "<p><33> <__></p>\n", "example": 617, "start_line": 9055, "end_line": 9059, "section": "Raw HTML" }, { "markdown": "<a h*#ref=\"hi\">\n", "html": "<p><a h*#ref="hi"></p>\n", "example": 618, "start_line": 9064, "end_line": 9068, "section": "Raw HTML" }, { "markdown": "<a href=\"hi'> <a href=hi'>\n", "html": "<p><a href="hi'> <a href=hi'></p>\n", "example": 619, "start_line": 9073, "end_line": 9077, "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": 620, "start_line": 9082, "end_line": 9092, "section": "Raw HTML" }, { "markdown": "<a href='bar'title=title>\n", "html": "<p><a href='bar'title=title></p>\n", "example": 621, "start_line": 9097, "end_line": 9101, "section": "Raw HTML" }, { "markdown": "</a></foo >\n", "html": "<p></a></foo ></p>\n", "example": 622, "start_line": 9106, "end_line": 9110, "section": "Raw HTML" }, { "markdown": "</a href=\"foo\">\n", "html": "<p></a href="foo"></p>\n", "example": 623, "start_line": 9115, "end_line": 9119, "section": "Raw HTML" }, { "markdown": "foo <!-- this is a\ncomment - with hyphen -->\n", "html": "<p>foo <!-- this is a\ncomment - with hyphen --></p>\n", "example": 624, "start_line": 9124, "end_line": 9130, "section": "Raw HTML" }, { "markdown": "foo <!-- not a comment -- two hyphens -->\n", "html": "<p>foo <!-- not a comment -- two hyphens --></p>\n", "example": 625, "start_line": 9133, "end_line": 9137, "section": "Raw HTML" }, { "markdown": "foo <!--> foo -->\n\nfoo <!-- foo--->\n", "html": "<p>foo <!--> foo --></p>\n<p>foo <!-- foo---></p>\n", "example": 626, "start_line": 9142, "end_line": 9149, "section": "Raw HTML" }, { "markdown": "foo <?php echo $a; ?>\n", "html": "<p>foo <?php echo $a; ?></p>\n", "example": 627, "start_line": 9154, "end_line": 9158, "section": "Raw HTML" }, { "markdown": "foo <!ELEMENT br EMPTY>\n", "html": "<p>foo <!ELEMENT br EMPTY></p>\n", "example": 628, "start_line": 9163, "end_line": 9167, "section": "Raw HTML" }, { "markdown": "foo <![CDATA[>&<]]>\n", "html": "<p>foo <![CDATA[>&<]]></p>\n", "example": 629, "start_line": 9172, "end_line": 9176, "section": "Raw HTML" }, { "markdown": "foo <a href=\"ö\">\n", "html": "<p>foo <a href=\"ö\"></p>\n", "example": 630, "start_line": 9182, "end_line": 9186, "section": "Raw HTML" }, { "markdown": "foo <a href=\"\\*\">\n", "html": "<p>foo <a href=\"\\*\"></p>\n", "example": 631, "start_line": 9191, "end_line": 9195, "section": "Raw HTML" }, { "markdown": "<a href=\"\\\"\">\n", "html": "<p><a href="""></p>\n", "example": 632, "start_line": 9198, "end_line": 9202, "section": "Raw HTML" }, { "markdown": "foo \nbaz\n", "html": "<p>foo<br />\nbaz</p>\n", "example": 633, "start_line": 9212, "end_line": 9218, "section": "Hard line breaks" }, { "markdown": "foo\\\nbaz\n", "html": "<p>foo<br />\nbaz</p>\n", "example": 634, "start_line": 9224, "end_line": 9230, "section": "Hard line breaks" }, { "markdown": "foo \nbaz\n", "html": "<p>foo<br />\nbaz</p>\n", "example": 635, "start_line": 9235, "end_line": 9241, "section": "Hard line breaks" }, { "markdown": "foo \n bar\n", "html": "<p>foo<br />\nbar</p>\n", "example": 636, "start_line": 9246, "end_line": 9252, "section": "Hard line breaks" }, { "markdown": "foo\\\n bar\n", "html": "<p>foo<br />\nbar</p>\n", "example": 637, "start_line": 9255, "end_line": 9261, "section": "Hard line breaks" }, { "markdown": "*foo \nbar*\n", "html": "<p><em>foo<br />\nbar</em></p>\n", "example": 638, "start_line": 9267, "end_line": 9273, "section": "Hard line breaks" }, { "markdown": "*foo\\\nbar*\n", "html": "<p><em>foo<br />\nbar</em></p>\n", "example": 639, "start_line": 9276, "end_line": 9282, "section": "Hard line breaks" }, { "markdown": "`code \nspan`\n", "html": "<p><code>code span</code></p>\n", "example": 640, "start_line": 9287, "end_line": 9292, "section": "Hard line breaks" }, { "markdown": "`code\\\nspan`\n", "html": "<p><code>code\\ span</code></p>\n", "example": 641, "start_line": 9295, "end_line": 9300, "section": "Hard line breaks" }, { "markdown": "<a href=\"foo \nbar\">\n", "html": "<p><a href=\"foo \nbar\"></p>\n", "example": 642, "start_line": 9305, "end_line": 9311, "section": "Hard line breaks" }, { "markdown": "<a href=\"foo\\\nbar\">\n", "html": "<p><a href=\"foo\\\nbar\"></p>\n", "example": 643, "start_line": 9314, "end_line": 9320, "section": "Hard line breaks" }, { "markdown": "foo\\\n", "html": "<p>foo\\</p>\n", "example": 644, "start_line": 9327, "end_line": 9331, "section": "Hard line breaks" }, { "markdown": "foo \n", "html": "<p>foo</p>\n", "example": 645, "start_line": 9334, "end_line": 9338, "section": "Hard line breaks" }, { "markdown": "### foo\\\n", "html": "<h3>foo\\</h3>\n", "example": 646, "start_line": 9341, "end_line": 9345, "section": "Hard line breaks" }, { "markdown": "### foo \n", "html": "<h3>foo</h3>\n", "example": 647, "start_line": 9348, "end_line": 9352, "section": "Hard line breaks" }, { "markdown": "foo\nbaz\n", "html": "<p>foo\nbaz</p>\n", "example": 648, "start_line": 9363, "end_line": 9369, "section": "Soft line breaks" }, { "markdown": "foo \n baz\n", "html": "<p>foo\nbaz</p>\n", "example": 649, "start_line": 9375, "end_line": 9381, "section": "Soft line breaks" }, { "markdown": "hello $.;'there\n", "html": "<p>hello $.;'there</p>\n", "example": 650, "start_line": 9395, "end_line": 9399, "section": "Textual content" }, { "markdown": "Foo χρῆν\n", "html": "<p>Foo χρῆν</p>\n", "example": 651, "start_line": 9402, "end_line": 9406, "section": "Textual content" }, { "markdown": "Multiple spaces\n", "html": "<p>Multiple spaces</p>\n", "example": 652, "start_line": 9411, "end_line": 9415, "section": "Textual content" } ] |
Added testdata/testbox/00000000000100.zettel.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none expert-mode: true modified: 20210629174242 no-index: true visibility: owner |
Added testdata/testbox/19700101000000.zettel.
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | id: 19700101000000 title: Startup Configuration role: configuration tags: #invisible syntax: none box-uri-1: mem: box-uri-2: dir:testdata/testbox?readonly modified: 20210629174022 owner: 20210629163300 token-lifetime-api: 1 visibility: owner |
Added testdata/testbox/20210629163300.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | id: 20210629163300 title: owner role: user tags: #test #user syntax: none credential: $2a$10$gcKyVmQ50fwgpOjyiiCm4eba/ILrNXoxTUCopgTEnYTa4yuceHMC6 modified: 20210629173617 user-id: owner |
Added testdata/testbox/20210629165000.zettel.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | id: 20210629165000 title: writer role: user tags: #test #user syntax: none credential: $2a$10$VmHPyXa0Bm8DE4MJ.pQnbuuQmweWtyGya0L/bFA4nIuCn1EvPQflK modified: 20210629173536 user-id: writer user-role: writer |
Added testdata/testbox/20210629165024.zettel.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | id: 20210629165024 title: reader role: user tags: #test #user syntax: none credential: $2a$10$uC7LV2JdFhasw2HqSWZbSOihvFpwtaEXjXp98yzGfE3FHudq.vg.u modified: 20210629173459 user-id: reader user-role: reader |
Added testdata/testbox/20210629165050.zettel.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | id: 20210629165050 title: creator role: user tags: #test #user syntax: none credential: $2a$10$z85253tqhbHlXPZpt0hJpughLR4WXY8iYJbm1LlBhrKsL1YfkRy2q modified: 20210629173424 user-id: creator user-role: creator |
Changes to tests/markdown_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 | "encoding/json" "fmt" "os" "regexp" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" | > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "encoding/json" "fmt" "os" "regexp" "strings" "testing" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" |
︙ | ︙ | |||
40 41 42 43 44 45 46 | 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 | | | | | | | | | | | | | | | | | < | | | > | > > | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | EndLine int `json:"end_line"` Section string `json:"section"` } // exceptions lists all CommonMark tests that should not be tested for identical HTML output var exceptions = []string{ " - foo\n - bar\n\t - baz\n", // 9 "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n", // 170 "<script>\nfoo\n</script>1. *bar*\n", // 178 "- foo\n - bar\n - baz\n - boo\n", // 294 "10) foo\n - bar\n", // 296 "- # Foo\n- Bar\n ---\n baz\n", // 300 "- foo\n\n- bar\n\n\n- baz\n", // 306 "- foo\n - bar\n - baz\n\n\n bim\n", // 307 "1. a\n\n 2. b\n\n 3. c\n", // 311 "1. a\n\n 2. b\n\n 3. c\n", // 313 "- a\n- b\n\n- c\n", // 314 "* a\n*\n\n* c\n", // 315 "- a\n- b\n\n [ref]: /url\n- d\n", // 317 "- a\n - b\n\n c\n- d\n", // 319 "* a\n > b\n >\n* c\n", // 320 "- a\n > b\n ```\n c\n ```\n- d\n", // 321 "- a\n - b\n", // 323 "<http://foo.bar.`baz>`\n", // 345 "[foo<http://example.com/?search=](uri)>\n", // 525 "[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 537 "<http://example.com?find=\\*>\n", // 581 "<http://foo.bar.baz/test?q=hello&id=22&boolean>\n", // 594 } var reHeadingID = regexp.MustCompile(` id="[^"]*"`) func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, format := range formats { enc := encoder.Create(format, nil) if enc == nil { t.Errorf("No encoder for %q found", format) encoderMissing = true } } if encoderMissing { panic("At least one encoder is missing. See test log") } } func TestMarkdownSpec(t *testing.T) { t.Parallel() content, err := os.ReadFile("../testdata/markdown/spec.json") if err != nil { panic(err) } var testcases []markdownTestCase if err = json.Unmarshal(content, &testcases); err != nil { panic(err) |
︙ | ︙ | |||
114 115 116 117 118 119 120 | encoder.Create(format, nil).WriteBlocks(&sb, ast) sb.Reset() }) } } func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { | | | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | encoder.Create(format, nil).WriteBlocks(&sb, ast) sb.Reset() }) } } func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { htmlEncoder := encoder.Create(api.EncoderHTML, &encoder.Environment{Xhtml: true}) var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) { htmlEncoder.WriteBlocks(&sb, ast) gotHTML := sb.String() sb.Reset() |
︙ | ︙ | |||
139 140 141 142 143 144 145 | st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML) } } }) } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { | | | 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML) } } }) } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { zmkEncoder := encoder.Create(api.EncoderZmk, nil) var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { zmkEncoder.WriteBlocks(&sb, ast) gotFirst := sb.String() sb.Reset() |
︙ | ︙ |
Changes to tests/regression_test.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "io" "net/url" "os" "path/filepath" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" | > > > < < > < | > > > > | > | | | | | | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | "io" "net/url" "os" "path/filepath" "strings" "testing" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" _ "zettelstore.de/z/box/dirbox" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" _ "zettelstore.de/z/parser/blob" _ "zettelstore.de/z/parser/zettelmark" ) var formats = []api.EncodingEnum{ api.EncoderHTML, api.EncoderDJSON, api.EncoderNative, api.EncoderText, } func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { panic(err) } cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple) if err != nil { panic(err) } box, err := manager.Connect(u, &noAuth{}, &cdata) if err != nil { panic(err) } boxes = append(boxes, box) } } return root, boxes } type noEnrich struct{} func (nf *noEnrich) Enrich(context.Context, *meta.Meta, int) {} func (nf *noEnrich) Remove(context.Context, *meta.Meta) {} type noAuth struct{} func (na *noAuth) IsReadonly() bool { return false } func trimLastEOL(s string) string { if lastPos := len(s) - 1; lastPos >= 0 && s[lastPos] == '\n' { |
︙ | ︙ | |||
89 90 91 92 93 94 95 | return "", err } defer f.Close() src, err := io.ReadAll(f) return string(src), err } | | | | | | | | > | | | | | | | | 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | return "", err } defer f.Close() src, err := io.ReadAll(f) return string(src), err } func checkFileContent(t *testing.T, filename, gotContent string) { t.Helper() wantContent, err := resultFile(filename) if err != nil { t.Error(err) return } gotContent = trimLastEOL(gotContent) wantContent = trimLastEOL(wantContent) if gotContent != wantContent { t.Errorf("\nWant: %q\nGot: %q", wantContent, gotContent) } } func checkBlocksFile(t *testing.T, resultName string, zn *ast.ZettelNode, format api.EncodingEnum) { t.Helper() var env encoder.Environment if enc := encoder.Create(format, &env); enc != nil { var sb strings.Builder enc.WriteBlocks(&sb, zn.Ast) checkFileContent(t, resultName, sb.String()) return } panic(fmt.Sprintf("Unknown writer format %q", format)) } func checkZmkEncoder(t *testing.T, zn *ast.ZettelNode) { zmkEncoder := encoder.Create(api.EncoderZmk, nil) var sb strings.Builder zmkEncoder.WriteBlocks(&sb, zn.Ast) gotFirst := sb.String() sb.Reset() newZettel := parser.ParseZettel(domain.Zettel{ Meta: zn.Meta, Content: domain.NewContent("\n" + gotFirst)}, "", testConfig) zmkEncoder.WriteBlocks(&sb, newZettel.Ast) gotSecond := sb.String() sb.Reset() if gotFirst != gotSecond { t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) } } func getBoxName(p box.ManagedBox, root string) string { u, err := url.Parse(p.Location()) if err != nil { panic("Unable to parse URL '" + p.Location() + "': " + err.Error()) } return u.Path[len(root):] } func match(*meta.Meta) bool { return true } func checkContentBox(t *testing.T, p box.ManagedBox, wd, boxName string) { ss := p.(box.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList, err := p.SelectMeta(context.Background(), match) if err != nil { panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } z := parser.ParseZettel(zettel, "", testConfig) for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { resultName := filepath.Join(wd, "result", "content", boxName, z.Zid.String()+"."+format.String()) checkBlocksFile(st, resultName, z, format) }) } t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) { checkZmkEncoder(st, z) }) } if err := ss.Stop(context.Background()); err != nil { panic(err) } } func TestContentRegression(t *testing.T) { t.Parallel() wd, err := os.Getwd() if err != nil { panic(err) } root, boxes := getFileBoxes(wd, "content") for _, p := range boxes { checkContentBox(t, p, wd, getBoxName(p, root)) } } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format api.EncodingEnum) { t.Helper() if enc := encoder.Create(format, nil); enc != nil { var sb strings.Builder enc.WriteMeta(&sb, zn.Meta) checkFileContent(t, resultName, sb.String()) return } panic(fmt.Sprintf("Unknown writer format %q", format)) } func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) { ss := p.(box.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList, err := p.SelectMeta(context.Background(), match) if err != nil { panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } z := parser.ParseZettel(zettel, "", testConfig) for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+format.String()) checkMetaFile(st, resultName, z, format) }) } } if err := ss.Stop(context.Background()); err != nil { panic(err) } |
︙ | ︙ | |||
245 246 247 248 249 250 251 252 253 254 255 | func (cfg *myConfig) GetExpertMode() bool { return false } func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() } var testConfig = &myConfig{} func TestMetaRegression(t *testing.T) { wd, err := os.Getwd() if err != nil { panic(err) } | > | | | | 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | func (cfg *myConfig) GetExpertMode() bool { return false } func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() } var testConfig = &myConfig{} func TestMetaRegression(t *testing.T) { t.Parallel() wd, err := os.Getwd() if err != nil { panic(err) } root, boxes := getFileBoxes(wd, "meta") for _, p := range boxes { checkMetaBox(t, p, wd, getBoxName(p, root)) } } |
Changes to tools/build.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // Package main provides a command to build and run the software. package main import ( "archive/zip" "bytes" "flag" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "regexp" "strings" "time" "zettelstore.de/z/strfun" ) func executeCommand(env []string, name string, arg ...string) (string, error) { if verbose { if len(env) > 0 { for i, e := range env { fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) } } | > > > > > > > > > > > > > > > > > > > > > > | < < < < < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | // Package main provides a command to build and run the software. package main import ( "archive/zip" "bytes" "errors" "flag" "fmt" "io" "io/fs" "net" "os" "os/exec" "path/filepath" "regexp" "strings" "time" "zettelstore.de/z/strfun" ) func executeCommand(env []string, name string, arg ...string) (string, error) { logCommand("EXEC", env, name, arg) var out bytes.Buffer cmd := prepareCommand(env, name, arg, &out) err := cmd.Run() return out.String(), err } func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd { if len(env) > 0 { env = append(env, os.Environ()...) } cmd := exec.Command(name, arg...) cmd.Env = env cmd.Stdin = nil cmd.Stdout = out cmd.Stderr = os.Stderr return cmd } func logCommand(exec string, env []string, name string, arg []string) { if verbose { if len(env) > 0 { for i, e := range env { fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) } } fmt.Fprintln(os.Stderr, exec, name, arg) } } func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { return "", err } |
︙ | ︙ | |||
120 121 122 123 124 125 126 | if path, err := executeCommand(nil, "which", "shadow"); err == nil && path != "" { return path } return "" } func cmdCheck() error { | | | > > | | 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 | if path, err := executeCommand(nil, "which", "shadow"); err == nil && path != "" { return path } return "" } func cmdCheck() error { if err := checkGoTest("./..."); err != nil { return err } if err := checkGoVet(); err != nil { return err } if err := checkGoLint(); err != nil { return err } if err := checkGoVetShadow(); err != nil { return err } if err := checkStaticcheck(); err != nil { return err } return checkFossilExtra() } func checkGoTest(pkg string, testParams ...string) error { args := []string{"test", pkg} args = append(args, testParams...) out, err := executeCommand(nil, "go", args...) if err != nil { for _, line := range strfun.SplitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } fmt.Fprintln(os.Stderr, line) } |
︙ | ︙ | |||
214 215 216 217 218 219 220 221 222 223 224 225 226 227 | } fmt.Fprintf(os.Stderr, " %q", extra) } fmt.Fprintln(os.Stderr) } return nil } func cmdBuild() error { return doBuild(nil, getVersion(), "bin/zettelstore") } func doBuild(env []string, version, target string) error { out, err := executeCommand( | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | } fmt.Fprintf(os.Stderr, " %q", extra) } fmt.Fprintln(os.Stderr) } return nil } type zsInfo struct { cmd *exec.Cmd out bytes.Buffer adminAddress string } func cmdTestAPI() error { var err error var info zsInfo needServer := !addressInUse(":23123") if needServer { err = startZettelstore(&info) } if err != nil { return err } err = checkGoTest("zettelstore.de/z/client", "-base-url", "http://127.0.0.1:23123") if needServer { err1 := stopZettelstore(&info) if err == nil { err = err1 } } return err } func startZettelstore(info *zsInfo) error { info.adminAddress = ":2323" name, arg := "go", []string{ "run", "cmd/zettelstore/main.go", "run", "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]} logCommand("FORK", nil, name, arg) cmd := prepareCommand(nil, name, arg, &info.out) if !verbose { cmd.Stderr = nil } err := cmd.Start() for i := 0; i < 100; i++ { time.Sleep(time.Millisecond * 100) if addressInUse(info.adminAddress) { info.cmd = cmd return err } } return errors.New("zettelstore did not start") } func stopZettelstore(i *zsInfo) error { conn, err := net.Dial("tcp", i.adminAddress) if err != nil { fmt.Println("Unable to stop Zettelstore") return err } io.WriteString(conn, "shutdown\n") conn.Close() err = i.cmd.Wait() return err } func addressInUse(address string) bool { conn, err := net.Dial("tcp", address) if err != nil { return false } conn.Close() return true } func cmdBuild() error { return doBuild(nil, getVersion(), "bin/zettelstore") } func doBuild(env []string, version, target string) error { out, err := executeCommand( |
︙ | ︙ | |||
406 407 408 409 410 411 412 413 414 415 416 417 418 419 | extra files, ... Is automatically done when releasing the software. clean Remove all build and release directories. help Outputs this text. manual Create a ZIP file with all manual zettel release Create the software for various platforms and put them in appropriate named ZIP files. version Print the current version of the software. All commands can be abbreviated as long as they remain unique.`) } var ( verbose bool | > | 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 | extra files, ... Is automatically done when releasing the software. clean Remove all build and release directories. help Outputs this text. manual Create a ZIP file with all manual zettel release Create the software for various platforms and put them in appropriate named ZIP files. testapi Starts a Zettelstore and execute API tests. version Print the current version of the software. All commands can be abbreviated as long as they remain unique.`) } var ( verbose bool |
︙ | ︙ | |||
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 | err = cmdRelease() case "cl", "cle", "clea", "clean": err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": err = cmdCheck() case "h", "he", "hel", "help": cmdHelp() default: fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) cmdHelp() os.Exit(1) } } if err != nil { fmt.Fprintln(os.Stderr, err) } } | > > | 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 | err = cmdRelease() case "cl", "cle", "clea", "clean": err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": err = cmdCheck() case "t", "te", "tes", "test", "testa", "testap", "testapi": cmdTestAPI() case "h", "he", "hel", "help": cmdHelp() default: fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) cmdHelp() os.Exit(1) } } if err != nil { fmt.Fprintln(os.Stderr, err) } } |
Changes to usecase/context.go.
︙ | ︙ | |||
41 42 43 44 45 46 47 | const ( _ ZettelContextDirection = iota ZettelContextForward // Traverse all forwarding links ZettelContextBackward // Traverse all backwaring links ZettelContextBoth // Traverse both directions ) | < < < < < < < < < < < | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | const ( _ ZettelContextDirection = iota ZettelContextForward // Traverse all forwarding links ZettelContextBackward // Traverse all backwaring links ZettelContextBoth // Traverse both directions ) // Run executes the use case. func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) { start, err := uc.port.GetMeta(ctx, zid) if err != nil { return nil, err } tasks := ztlCtx{depth: depth} |
︙ | ︙ |
Changes to usecase/copy_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" | < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 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 { |
︙ | ︙ | |||
32 33 34 35 36 37 38 | if len(title) > 0 { title = "Copy of " + title } else { title = "Copy" } m.Set(meta.KeyTitle, title) } | | > | | 31 32 33 34 35 36 37 38 39 40 41 | if len(title) > 0 { title = "Copy of " + title } else { title = "Copy" } m.Set(meta.KeyTitle, title) } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } |
Changes to usecase/create_zettel.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "context" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "context" "zettelstore.de/z/config" "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) } |
︙ | ︙ | |||
55 56 57 58 59 60 61 | m.Set(meta.KeyRole, uc.rtConfig.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) } m.YamlSep = uc.rtConfig.GetYAMLHeader() | | | 54 55 56 57 58 59 60 61 62 63 | m.Set(meta.KeyRole, uc.rtConfig.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) } m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() return uc.port.CreateZettel(ctx, zettel) } |
Changes to usecase/delete_zettel.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "context" "zettelstore.de/z/domain/id" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "context" "zettelstore.de/z/domain/id" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } // DeleteZettel is the data for this use case. type DeleteZettel struct { port DeleteZettelPort } |
︙ | ︙ |
Changes to usecase/folge_zettel.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
40 41 42 43 44 45 46 | } m.Set(meta.KeyTitle, title) } m.Set(meta.KeyRole, config.GetRole(origMeta, uc.rtConfig)) m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) m.Set(meta.KeyPrecursor, origMeta.Zid.String()) | | | 40 41 42 43 44 45 46 47 48 | } m.Set(meta.KeyTitle, title) } m.Set(meta.KeyRole, config.GetRole(origMeta, uc.rtConfig)) m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) m.Set(meta.KeyPrecursor, origMeta.Zid.String()) return domain.Zettel{Meta: m, Content: domain.NewContent("")} } |
Added usecase/get_all_meta.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // GetAllMetaPort is the interface used by this use case. type GetAllMetaPort interface { // GetAllMeta retrieves just the meta data of a specific zettel. GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) } // GetAllMeta is the data for this use case. type GetAllMeta struct { port GetAllMetaPort } // NewGetAllMeta creates a new use case. func NewGetAllMeta(port GetAllMetaPort) GetAllMeta { return GetAllMeta{port: port} } // Run executes the use case. func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { return uc.port.GetAllMeta(ctx, zid) } |
Changes to usecase/get_user.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/auth" | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. |
︙ | ︙ | |||
39 40 41 42 43 44 45 | // NewGetUser creates a new use case. func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser { return GetUser{authz: authz, port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // NewGetUser creates a new use case. func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser { return GetUser{authz: authz, port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) if err == nil && identMeta.GetDefault(meta.KeyUserID, "") == ident { if role, ok := identMeta.Get(meta.KeyRole); !ok || |
︙ | ︙ | |||
86 87 88 89 90 91 92 | // NewGetUserByZid creates a new use case. func NewGetUserByZid(port GetUserByZidPort) GetUserByZid { return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | // NewGetUserByZid creates a new use case. func NewGetUserByZid(port GetUserByZidPort) GetUserByZid { return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } |
Changes to usecase/list_role.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "sort" | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "sort" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // ListRolePort is the interface used by this use case. type ListRolePort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) |
︙ | ︙ | |||
34 35 36 37 38 39 40 | // 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) { | | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // 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(box.NoEnrichContext(ctx), nil) if err != nil { return nil, err } roles := make(map[string]bool, 8) for _, m := range metas { if role, ok := m.Get(meta.KeyRole); ok && role != "" { roles[role] = true |
︙ | ︙ |
Changes to usecase/list_tags.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // ListTagsPort is the interface used by this use case. type ListTagsPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) |
︙ | ︙ | |||
36 37 38 39 40 41 42 | } // 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) { | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | } // TagData associates tags with a list of all zettel meta that use this tag type TagData map[string][]*meta.Meta // Run executes the use case. func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil) 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 { |
︙ | ︙ |
Changes to usecase/new_zettel.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" | | > > | > > > > > | | | | < | > | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // 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 := meta.New(id.Invalid) om := origZettel.Meta m.Set(meta.KeyTitle, om.GetDefault(meta.KeyTitle, "")) m.Set(meta.KeyRole, om.GetDefault(meta.KeyRole, "")) m.Set(meta.KeyTags, om.GetDefault(meta.KeyTags, "")) m.Set(meta.KeySyntax, om.GetDefault(meta.KeySyntax, "")) const prefixLen = len(meta.NewPrefix) for _, pair := range om.PairsRest(false) { if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { m.Set(key[prefixLen:], pair.Value) } } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } |
Changes to usecase/order.go.
︙ | ︙ | |||
33 34 35 36 37 38 39 | // NewZettelOrder creates a new use case. func NewZettelOrder(port ZettelOrderPort, parseZettel ParseZettel) ZettelOrder { return ZettelOrder{port: port, parseZettel: parseZettel} } // Run executes the use case. | | < | > | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | // NewZettelOrder creates a new use case. func NewZettelOrder(port ZettelOrderPort, parseZettel ParseZettel) ZettelOrder { return ZettelOrder{port: port, parseZettel: parseZettel} } // Run executes the use case. func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) ( start *meta.Meta, result []*meta.Meta, err error, ) { zn, err := uc.parseZettel.Run(ctx, zid, syntax) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { if zid, err := id.Parse(ref.URL.Path); err == nil { if m, err := uc.port.GetMeta(ctx, zid); err == nil { result = append(result, m) } } } return zn.Meta, result, nil } |
Changes to usecase/rename_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | | | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // 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 box operation. type ErrZidInUse struct{ Zid id.Zid } func (err *ErrZidInUse) Error() string { return "Zettel id already in use: " + err.Zid.String() } // NewRenameZettel creates a new use case. func NewRenameZettel(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 := box.NoEnrichContext(ctx) if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { return err } if newZid == curZid { // Nothing to do return nil } if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil { return &ErrZidInUse{Zid: newZid} } return uc.port.RenameZettel(ctx, curZid, newZid) } |
Changes to usecase/search.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // SearchPort is the interface used by this use case. type SearchPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) |
︙ | ︙ | |||
34 35 36 37 38 39 40 | func NewSearch(port SearchPort) Search { return Search{port: port} } // Run executes the use case. func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { | | | 34 35 36 37 38 39 40 41 42 43 44 | func NewSearch(port SearchPort) Search { return Search{port: port} } // Run executes the use case. func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = box.NoEnrichContext(ctx) } return uc.port.SelectMeta(ctx, s) } |
Changes to usecase/update_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 | // 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" | > < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // 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) |
︙ | ︙ | |||
39 40 41 42 43 44 45 | 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 | | > | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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(box.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 zettel.Content.TrimSpace() } return uc.port.UpdateZettel(ctx, zettel) } |
Changes to web/adapter/api/api.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package api provides api handlers for web requests. package api import ( "context" "time" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) | > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package api provides api handlers for web requests. package api import ( "context" "time" "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) |
︙ | ︙ | |||
47 48 49 50 51 52 53 | return api } // GetURLPrefix returns the configured URL prefix of the web server. func (api *API) GetURLPrefix() string { return api.b.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | return api } // GetURLPrefix returns the configured URL prefix of the web server. func (api *API) GetURLPrefix() string { return api.b.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (api *API) NewURLBuilder(key byte) *api.URLBuilder { return api.b.NewURLBuilder(key) } func (api *API) getAuthData(ctx context.Context) *server.AuthData { return api.auth.GetAuthData(ctx) } func (api *API) withAuth() bool { return api.authz.WithAuth() } func (api *API) getToken(ident *meta.Meta) ([]byte, error) { return api.token.GetToken(ident, api.tokenLifetime, auth.KindJSON) } |
Changes to web/adapter/api/content_type.go.
1 | //----------------------------------------------------------------------------- | | > > | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import "zettelstore.de/z/api" const plainText = "text/plain; charset=utf-8" var mapFormat2CT = map[api.EncodingEnum]string{ api.EncoderHTML: "text/html; charset=utf-8", api.EncoderNative: plainText, api.EncoderJSON: "application/json", api.EncoderDJSON: "application/json", api.EncoderText: plainText, api.EncoderZmk: plainText, api.EncoderRaw: plainText, // In some cases... } func format2ContentType(format api.EncodingEnum) string { ct, ok := mapFormat2CT[format] if !ok { return "application/octet-stream" } return ct } |
︙ | ︙ |
Added web/adapter/api/create_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (api *API) MakePostCreateZettelHandler(createZettel usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zettel, err := buildZettelFromData(r, id.Invalid) if err != nil { adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { adapter.ReportUsecaseError(w, err) return } u := api.NewURLBuilder('z').SetZid(newZid).String() h := w.Header() h.Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) h.Set(zsapi.HeaderLocation, u) w.WriteHeader(http.StatusCreated) if err = encodeJSONData(w, zsapi.ZidJSON{ID: newZid.String(), URL: u}); err != nil { adapter.InternalServerError(w, "Write JSON", err) } } } |
Added web/adapter/api/delete_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (api *API) MakeDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } if err := deleteZettel.Run(r.Context(), zid); err != nil { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } |
Changes to web/adapter/api/get_links.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( | < > > < < < < < < < < < < < < < < < < < | | > > > > > > > | < < | | > | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "strconv" zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetLinksHandler creates a new API handler to return links to other material. func (api *API) MakeGetLinksHandler(parseZettel usecase.ParseZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) 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 := zsapi.ZettelLinksJSON{ ID: zid.String(), URL: api.NewURLBuilder('z').SetZid(zid).String(), } if kind&kindLink != 0 { api.setupLinkJSONRefs(summary, matter, &outData) if matter&matterMeta != 0 { for _, p := range zn.Meta.PairsRest(false) { if meta.Type(p.Key) == meta.TypeURL { outData.Links.Meta = append(outData.Links.Meta, p.Value) } } } } if kind&kindImage != 0 { api.setupImageJSONRefs(summary, matter, &outData) } if kind&kindCite != 0 { outData.Cites = stringCites(summary.Cites) } w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) encodeJSONData(w, outData) } } func (api *API) setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *zsapi.ZettelLinksJSON) { if matter&matterIncoming != 0 { // TODO: calculate incoming links from other zettel (via "backward" metadata?) outData.Links.Incoming = []zsapi.ZidJSON{} } zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links) if matter&matterOutgoing != 0 { outData.Links.Outgoing = api.idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Links.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Links.External = stringRefs(extRefs) } } func (api *API) setupImageJSONRefs(summary collect.Summary, matter matterType, outData *zsapi.ZettelLinksJSON) { zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Images) if matter&matterOutgoing != 0 { outData.Images.Outgoing = api.idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Images.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Images.External = stringRefs(extRefs) } } func (api *API) idURLRefs(refs []*ast.Reference) []zsapi.ZidJSON { result := make([]zsapi.ZidJSON, 0, len(refs)) for _, ref := range refs { path := ref.URL.Path ub := api.NewURLBuilder('z').AppendPath(path) if fragment := ref.URL.Fragment; len(fragment) > 0 { ub.SetFragment(fragment) } result = append(result, zsapi.ZidJSON{ID: path, URL: ub.String()}) } return result } func stringRefs(refs []*ast.Reference) []string { result := make([]string, 0, len(refs)) for _, ref := range refs { |
︙ | ︙ | |||
179 180 181 182 183 184 185 186 187 188 | const ( _ matterType = 1 << iota matterIncoming matterOutgoing matterLocal matterExternal ) var mapMatter = map[string]matterType{ | > | > | | | 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 | const ( _ matterType = 1 << iota matterIncoming matterOutgoing matterLocal matterExternal matterMeta ) var mapMatter = map[string]matterType{ "": matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta, "incoming": matterIncoming, "outgoing": matterOutgoing, "local": matterLocal, "external": matterExternal, "meta": matterMeta, "zettel": matterIncoming | matterOutgoing, "material": matterLocal | matterExternal | matterMeta, "all": matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta, } func getMatterFromValue(value string) matterType { if m, ok := mapMatter[value]; ok { return m } if n, err := strconv.Atoi(value); err == nil && n > 0 { |
︙ | ︙ |
Changes to web/adapter/api/get_order.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetOrderHandler creates a new API handler to return zettel references // of a given zettel. func (api *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() | > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetOrderHandler creates a new API handler to return zettel references // of a given zettel. func (api *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() start, metas, err := zettelOrder.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { adapter.ReportUsecaseError(w, err) return } api.writeMetaList(w, start, metas) } } |
Changes to web/adapter/api/get_role_list.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" | | | | | | | | < < < < < < < < < < < < < < < < < < < < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel". func (api *API) MakeListRoleHandler(listRole usecase.ListRole) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { roleList, err := listRole.Run(r.Context()) if err != nil { adapter.ReportUsecaseError(w, err) return } format, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) switch format { case zsapi.EncoderJSON: w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) encodeJSONData(w, zsapi.RoleListJSON{Roles: roleList}) default: adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", formatText)) } } } |
Changes to web/adapter/api/get_tags_list.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" | < | | | | | < < < < < < | < < | | | | | < | < < < < < < < < | < < | | < > | < < < | < | < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "strconv" zsapi "zettelstore.de/z/api" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel". func (api *API) MakeListTagsHandler(listTags usecase.ListTags) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(r.Context(), iMinCount) if err != nil { adapter.ReportUsecaseError(w, err) return } format, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) switch format { case zsapi.EncoderJSON: w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) tagMap := make(map[string][]string, len(tagData)) for tag, metaList := range tagData { zidList := make([]string, 0, len(metaList)) for _, m := range metaList { zidList = append(zidList, m.Zid.String()) } tagMap[tag] = zidList } encodeJSONData(w, zsapi.TagListJSON{Tags: tagMap}) default: adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", formatText)) } } } |
Changes to web/adapter/api/get_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 | package api import ( "errors" "fmt" "net/http" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" | > > < | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | package api import ( "errors" "fmt" "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel. func (api *API) MakeGetZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() format, _ := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) if format == zsapi.EncoderRaw { ctx = box.NoEnrichContext(ctx) } zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { adapter.ReportUsecaseError(w, err) return } part := getPart(q, partZettel) if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } switch format { case zsapi.EncoderJSON, zsapi.EncoderDJSON: w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) err = api.getWriteMetaZettelFunc(ctx, format, part, partZettel, getMeta)(w, zn) if err != nil { adapter.InternalServerError(w, "Write D/JSON", err) } return } |
︙ | ︙ | |||
86 87 88 89 90 91 92 | return } adapter.InternalServerError(w, "Get zettel", err) } } } | | | | | | | | | | | | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | return } adapter.InternalServerError(w, "Get zettel", err) } } } func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error { enc := encoder.Create(format, &env) if enc == nil { return adapter.ErrNoSuchFormat } inhMeta := false if format != zsapi.EncoderRaw { w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) inhMeta = true } _, err := enc.WriteZettel(w, zn, inhMeta) return err } func writeZettelPartMeta(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum) error { w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) if enc := encoder.Create(format, nil); enc != nil { if format == zsapi.EncoderRaw { _, err := enc.WriteMeta(w, zn.Meta) return err } _, err := enc.WriteMeta(w, zn.InhMeta) return err } return adapter.ErrNoSuchFormat } func (api *API) writeZettelPartContent(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error { if format == zsapi.EncoderRaw { if ct, ok := syntax2contentType(config.GetSyntax(zn.Meta, api.rtConfig)); ok { w.Header().Add(zsapi.HeaderContentType, ct) } } else { w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) } return writeContent(w, zn, format, &env) } |
Changes to web/adapter/api/get_zettel_context.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (api *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() | > | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // Package api provides api handlers for web requests. package api import ( "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (api *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() dir := adapter.GetZCDirection(q.Get(zsapi.QueryKeyDir)) depth, ok := adapter.GetInteger(q, zsapi.QueryKeyDepth) if !ok || depth < 0 { depth = 5 } limit, ok := adapter.GetInteger(q, zsapi.QueryKeyLimit) if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { adapter.ReportUsecaseError(w, err) return } api.writeMetaList(w, metaList[0], metaList[1:]) } } |
Changes to web/adapter/api/get_zettel_list.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" | > > < | | | | | | | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". func (api *API) MakeListMetaHandler( listMeta usecase.ListMeta, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() s := adapter.GetSearch(q, false) format, formatText := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) part := getPart(q, partMeta) if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } ctx1 := ctx if format == zsapi.EncoderHTML || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) { ctx1 = box.NoEnrichContext(ctx1) } metaList, err := listMeta.Run(ctx1, s) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) switch format { case zsapi.EncoderHTML: api.renderListMetaHTML(w, metaList) case zsapi.EncoderJSON, zsapi.EncoderDJSON: api.renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel) case zsapi.EncoderNative, zsapi.EncoderRaw, zsapi.EncoderText, zsapi.EncoderZmk: adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", formatText)) default: adapter.BadRequest(w, fmt.Sprintf("Zettel list not available in format %q", formatText)) } } } func (api *API) renderListMetaHTML(w http.ResponseWriter, metaList []*meta.Meta) { env := encoder.Environment{Interactive: true} buf := encoder.NewBufWriter(w) buf.WriteStrings("<html lang=\"", api.rtConfig.GetDefaultLang(), "\">\n<body>\n<ul>\n") for _, m := range metaList { title := m.GetDefault(meta.KeyTitle, "") htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), zsapi.EncoderHTML, &env) if err != nil { adapter.InternalServerError(w, "Format HTML inlines", err) return } buf.WriteStrings( "<li><a href=\"", api.NewURLBuilder('z').SetZid(m.Zid).AppendQuery(zsapi.QueryKeyFormat, zsapi.FormatHTML).String(), "\">", htmlTitle, "</a></li>\n") } buf.WriteString("</ul>\n</body>\n</html>") buf.Flush() } |
Changes to web/adapter/api/json.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "context" "encoding/json" "io" "net/http" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) | > < < < < < < < < < < < < < < < < < < < < < < | | | | < < < < < < < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import ( "context" "encoding/json" "io" "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) type jsonContent struct { ID string `json:"id"` URL string `json:"url"` Encoding string `json:"encoding"` Content string `json:"content"` } var ( djsonMetaHeader = []byte(",\"meta\":") djsonContentHeader = []byte(",\"content\":") djsonHeader1 = []byte("{\"id\":\"") djsonHeader2 = []byte("\",\"url\":\"") |
︙ | ︙ | |||
82 83 84 85 86 87 88 | } if err == nil { _, err = io.WriteString(w, api.NewURLBuilder('z').SetZid(zid).String()) } if err == nil { _, err = w.Write(djsonHeader3) if err == nil { | | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | } if err == nil { _, err = io.WriteString(w, api.NewURLBuilder('z').SetZid(zid).String()) } if err == nil { _, err = w.Write(djsonHeader3) if err == nil { _, err = io.WriteString(w, zsapi.FormatDJSON) } } if err == nil { _, err = w.Write(djsonHeader4) } return err } func (api *API) renderListMetaXJSON( ctx context.Context, w http.ResponseWriter, metaList []*meta.Meta, format zsapi.EncodingEnum, part, defPart partType, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) { prepareZettel := api.getPrepareZettelFunc(ctx, parseZettel, part) writeZettel := api.getWriteMetaZettelFunc(ctx, format, part, defPart, getMeta) err := writeListXJSON(w, metaList, prepareZettel, writeZettel) |
︙ | ︙ | |||
120 121 122 123 124 125 126 | return func(m *meta.Meta) (*ast.ZettelNode, error) { return parseZettel.Run(ctx, m.Zid, "") } case partMeta, partID: return func(m *meta.Meta) (*ast.ZettelNode, error) { return &ast.ZettelNode{ Meta: m, | | | | | | | | | > | | | | | | | | | > | | | | | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | return func(m *meta.Meta) (*ast.ZettelNode, error) { return parseZettel.Run(ctx, m.Zid, "") } case partMeta, partID: return func(m *meta.Meta) (*ast.ZettelNode, error) { return &ast.ZettelNode{ Meta: m, Content: domain.NewContent(""), Zid: m.Zid, InhMeta: api.rtConfig.AddDefaultValues(m), Ast: nil, }, nil } } return nil } type writeZettelFunc func(io.Writer, *ast.ZettelNode) error func (api *API) getWriteMetaZettelFunc(ctx context.Context, format zsapi.EncodingEnum, part, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { switch part { case partZettel: return api.getWriteZettelFunc(ctx, format, defPart, getMeta) case partMeta: return api.getWriteMetaFunc(ctx, format) case partContent: return api.getWriteContentFunc(ctx, format, defPart, getMeta) case partID: return api.getWriteIDFunc(ctx, format) default: panic(part) } } func (api *API) getWriteZettelFunc(ctx context.Context, format zsapi.EncodingEnum, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { if format == zsapi.EncoderJSON { return func(w io.Writer, zn *ast.ZettelNode) error { content, encoding := zn.Content.Encode() return encodeJSONData(w, zsapi.ZettelJSON{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Meta: zn.InhMeta.Map(), Encoding: encoding, Content: content, }) } } enc := encoder.Create(zsapi.EncoderDJSON, nil) if enc == nil { panic("no DJSON encoder found") } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) if err != nil { return err } _, err = w.Write(djsonMetaHeader) if err != nil { return err } _, err = enc.WriteMeta(w, zn.InhMeta) if err != nil { return err } _, err = w.Write(djsonContentHeader) if err != nil { return err } err = writeContent(w, zn, zsapi.EncoderDJSON, &encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter( ctx, api, 'z', getMeta, partZettel.DefString(defPart), zsapi.EncoderDJSON), ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } func (api *API) getWriteMetaFunc(ctx context.Context, format zsapi.EncodingEnum) writeZettelFunc { if format == zsapi.EncoderJSON { return func(w io.Writer, zn *ast.ZettelNode) error { return encodeJSONData(w, zsapi.ZidMetaJSON{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Meta: zn.InhMeta.Map(), }) } } enc := encoder.Create(zsapi.EncoderDJSON, nil) if enc == nil { panic("no DJSON encoder found") } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) if err != nil { return err } _, err = w.Write(djsonMetaHeader) if err != nil { return err } _, err = enc.WriteMeta(w, zn.InhMeta) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } func (api *API) getWriteContentFunc(ctx context.Context, format zsapi.EncodingEnum, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { if format == zsapi.EncoderJSON { return func(w io.Writer, zn *ast.ZettelNode) error { content, encoding := zn.Content.Encode() return encodeJSONData(w, jsonContent{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Encoding: encoding, Content: content, }) } } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) if err != nil { return err } _, err = w.Write(djsonContentHeader) if err != nil { return err } err = writeContent(w, zn, zsapi.EncoderDJSON, &encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter( ctx, api, 'z', getMeta, partContent.DefString(defPart), zsapi.EncoderDJSON), ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } func (api *API) getWriteIDFunc(ctx context.Context, format zsapi.EncodingEnum) writeZettelFunc { if format == zsapi.EncoderJSON { return func(w io.Writer, zn *ast.ZettelNode) error { return encodeJSONData(w, zsapi.ZidJSON{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), }) } } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) |
︙ | ︙ | |||
305 306 307 308 309 310 311 | } if err == nil { _, err = w.Write(jsonListFooter) } return err } | | > | > > > > > > | > | | > > > > > > > > | | > | > > < | | 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 | } if err == nil { _, err = w.Write(jsonListFooter) } return err } func writeContent(w io.Writer, zn *ast.ZettelNode, format zsapi.EncodingEnum, env *encoder.Environment) error { enc := encoder.Create(format, env) if enc == nil { return adapter.ErrNoSuchFormat } _, err := enc.WriteContent(w, zn) return err } func encodeJSONData(w io.Writer, data interface{}) error { enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(data) } func (api *API) writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { outList := make([]zsapi.ZidMetaJSON, len(metaList)) for i, m := range metaList { outList[i].ID = m.Zid.String() outList[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String() outList[i].Meta = m.Map() } w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) return encodeJSONData(w, zsapi.ZidMetaRelatedList{ ID: m.Zid.String(), URL: api.NewURLBuilder('z').SetZid(m.Zid).String(), Meta: m.Map(), List: outList, }) } func buildZettelFromData(r *http.Request, zid id.Zid) (domain.Zettel, error) { var zettel domain.Zettel dec := json.NewDecoder(r.Body) var zettelData zsapi.ZettelDataJSON if err := dec.Decode(&zettelData); err != nil { return zettel, err } m := meta.New(zid) for k, v := range zettelData.Meta { m.Set(k, v) } zettel.Meta = m if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil { return zettel, err } return zettel, nil } |
Changes to web/adapter/api/login.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 | package api import ( "encoding/json" "net/http" "time" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) | > | | | | | < < < < | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | package api import ( "encoding/json" "net/http" "time" zsapi "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (api *API) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !api.withAuth() { w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error token, err = ucAuth.Run(r.Context(), ident, cred, api.tokenLifetime, auth.KindJSON) if err != nil { adapter.ReportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) writeJSONToken(w, string(token), api.tokenLifetime) } } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { return ident, cred } if ident, cred, ok := r.BasicAuth(); ok { return ident, cred } return "", "" } func writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) { je := json.NewEncoder(w) je.Encode(zsapi.AuthJSON{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (api *API) MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() authData := api.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) return } // Token is a little bit aged. Create a new one token, err := api.getToken(authData.User) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) writeJSONToken(w, string(token), api.tokenLifetime) } } |
Added web/adapter/api/rename_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "net/url" "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeRenameZettelHandler creates a new HTTP handler to update a zettel. func (api *API) MakeRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } newZid, found := getDestinationZid(r) if !found { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err := renameZettel.Run(r.Context(), zid, newZid); err != nil { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } func getDestinationZid(r *http.Request) (id.Zid, bool) { if values, ok := r.Header[api.HeaderDestination]; ok { for _, value := range values { if zid, ok := getZidFromURL(value); ok { return zid, true } } } return id.Invalid, false } var zidLength = len(id.VersionZid.Bytes()) func getZidFromURL(val string) (id.Zid, bool) { u, err := url.Parse(val) if err != nil { return id.Invalid, false } if len(u.Path) < zidLength { return id.Invalid, false } zid, err := id.Parse(u.Path[len(u.Path)-zidLength:]) if err != nil { return id.Invalid, false } return zid, true } |
Changes to web/adapter/api/request.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api | | > > > > | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/url" "zettelstore.de/z/api" ) type partType int const ( partUnknown partType = iota partID partMeta partContent partZettel ) var partMap = map[string]partType{ api.PartID: partID, api.PartMeta: partMeta, api.PartContent: partContent, api.PartZettel: partZettel, } func getPart(q url.Values, defPart partType) partType { p := q.Get(api.QueryKeyPart) if p == "" { return defPart } if part, ok := partMap[p]; ok { return part } return partUnknown |
︙ | ︙ |
Added web/adapter/api/update_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. func (api *API) MakeUpdateZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } zettel, err := buildZettelFromData(r, zid) if err != nil { adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } if err := updateZettel.Run(r.Context(), zettel, true); err != nil { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } |
Changes to web/adapter/encoding.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 | package adapter import ( "context" "errors" "strings" "zettelstore.de/z/ast" | > | | | | | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | package adapter import ( "context" "errors" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // ErrNoSuchFormat signals an unsupported encoding format var ErrNoSuchFormat = errors.New("no such format") // FormatInlines returns a string representation of the inline slice. func FormatInlines(is ast.InlineSlice, format api.EncodingEnum, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", ErrNoSuchFormat } var content strings.Builder _, err := enc.WriteInlines(&content, is) if err != nil { return "", err } return content.String(), nil } // MakeLinkAdapter creates an adapter to change a link node during encoding. func MakeLinkAdapter( ctx context.Context, b server.Builder, key byte, getMeta usecase.GetMeta, part string, format api.EncodingEnum, ) func(*ast.LinkNode) ast.InlineNode { return func(origLink *ast.LinkNode) ast.InlineNode { origRef := origLink.Ref if origRef == nil { return origLink } if origRef.State == ast.RefStateBased { |
︙ | ︙ | |||
66 67 68 69 70 71 72 | if origRef.State != ast.RefStateZettel { return origLink } zid, err := id.Parse(origRef.URL.Path) if err != nil { panic(err) } | | | | | | | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | if origRef.State != ast.RefStateZettel { return origLink } zid, err := id.Parse(origRef.URL.Path) if err != nil { panic(err) } _, err = getMeta.Run(box.NoEnrichContext(ctx), zid) if errors.Is(err, &box.ErrNotAllowed{}) { return &ast.FormatNode{ Kind: ast.FormatSpan, Attrs: origLink.Attrs, Inlines: origLink.Inlines, } } var newRef *ast.Reference if err == nil { ub := b.NewURLBuilder(key).SetZid(zid) if part != "" { ub.AppendQuery(api.QueryKeyPart, part) } if format != api.EncoderUnknown { ub.AppendQuery(api.QueryKeyFormat, format.String()) } if fragment := origRef.URL.EscapedFragment(); fragment != "" { ub.SetFragment(fragment) } newRef = ast.ParseReference(ub.String()) newRef.State = ast.RefStateFound |
︙ | ︙ | |||
113 114 115 116 117 118 119 | case ast.RefStateInvalid: return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateInvalid) case ast.RefStateZettel: zid, err := id.Parse(origImage.Ref.Value) if err != nil { panic(err) } | | | 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | case ast.RefStateInvalid: return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateInvalid) case ast.RefStateZettel: zid, err := id.Parse(origImage.Ref.Value) if err != nil { panic(err) } _, err = getMeta.Run(box.NoEnrichContext(ctx), zid) if err != nil { return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateBroken) } return createZettelImage(b, origImage, zid, ast.RefStateFound) } return origImage } |
︙ | ︙ |
Deleted web/adapter/login.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/request.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "net/http" "net/url" "strconv" "strings" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // GetInteger returns the integer value of the named query key. func GetInteger(q url.Values, key string) (int, bool) { s := q.Get(key) if s != "" { if val, err := strconv.Atoi(s); err == nil { return val, true } } return 0, false } | > > > > > > > > > > > > > > > > > > > < < < | | | | | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "log" "net/http" "net/url" "strconv" "strings" "zettelstore.de/z/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" "zettelstore.de/z/usecase" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { log.Println(err) return "", "", false } ident = strings.TrimSpace(r.PostFormValue("username")) cred = r.PostFormValue("password") if ident == "" { return "", "", false } return ident, cred, true } // GetInteger returns the integer value of the named query key. func GetInteger(q url.Values, key string) (int, bool) { s := q.Get(key) if s != "" { if val, err := strconv.Atoi(s); err == nil { return val, true } } return 0, false } // GetFormat returns the data format selected by the caller. func GetFormat(r *http.Request, q url.Values, defFormat api.EncodingEnum) (api.EncodingEnum, string) { format := q.Get(api.QueryKeyFormat) if len(format) > 0 { return api.Encoder(format), format } if format, ok := getOneFormat(r, api.HeaderAccept); ok { return api.Encoder(format), format } if format, ok := getOneFormat(r, api.HeaderContentType); ok { return api.Encoder(format), format } return defFormat, "*default*" } 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": api.FormatJSON, "text/html": api.FormatHTML, } func contentType2format(contentType string) (string, bool) { // TODO: only check before first ';' format, ok := mapCT2format[contentType] return format, ok } // GetSearch retrieves the specified search and sorting options from a query. func GetSearch(q url.Values, forSearch bool) (s *search.Search) { sortQKey, orderQKey, offsetQKey, limitQKey, negateQKey, sQKey := getQueryKeys(forSearch) for key, values := range q { switch key { case sortQKey, orderQKey: s = extractOrderFromQuery(values, s) case offsetQKey: |
︙ | ︙ | |||
138 139 140 141 142 143 144 | func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search { for _, val := range values { s = s.AddExpr(key, val) } return s } | > > > > > > > > > > > | 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search { for _, val := range values { s = s.AddExpr(key, val) } return s } // GetZCDirection returns a direction value for a given string. func GetZCDirection(s string) usecase.ZettelContextDirection { switch s { case api.DirBackward: return usecase.ZettelContextBackward case api.DirForward: return usecase.ZettelContextForward } return usecase.ZettelContextBoth } |
Changes to web/adapter/response.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "errors" "fmt" "log" "net/http" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "errors" "fmt" "log" "net/http" "zettelstore.de/z/box" "zettelstore.de/z/usecase" ) // ReportUsecaseError returns an appropriate HTTP status code for errors in use cases. func ReportUsecaseError(w http.ResponseWriter, err error) { code, text := CodeMessageFromError(err) if code == http.StatusInternalServerError { |
︙ | ︙ | |||
38 39 40 41 42 43 44 | // NewErrBadRequest creates an new bad request error. func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} } func (err *ErrBadRequest) Error() string { return err.Text } // CodeMessageFromError returns an appropriate HTTP status code and text from a given error. func CodeMessageFromError(err error) (int, string) { | | | | | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // NewErrBadRequest creates an new bad request error. func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} } func (err *ErrBadRequest) Error() string { return err.Text } // CodeMessageFromError returns an appropriate HTTP status code and text from a given error. func CodeMessageFromError(err error) (int, string) { if err == box.ErrNotFound { return http.StatusNotFound, http.StatusText(http.StatusNotFound) } if err1, ok := err.(*box.ErrNotAllowed); ok { return http.StatusForbidden, err1.Error() } if err1, ok := err.(*box.ErrInvalidID); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid) } if err1, ok := err.(*usecase.ErrZidInUse); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid) } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } if errors.Is(err, box.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) } if errors.Is(err, box.ErrConflict) { return http.StatusConflict, "Zettelstore operations conflicted" } return http.StatusInternalServerError, err.Error() } |
Changes to web/adapter/webui/create_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package webui import ( "context" "fmt" "net/http" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" | > > < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package webui import ( "context" "fmt" "net/http" "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetCopyZettelHandler creates a new HTTP handler to display the // HTML edit view of a copied zettel. func (wui *WebUI) MakeGetCopyZettelHandler(getZettel usecase.GetZettel, copyZettel usecase.CopyZettel) http.HandlerFunc { |
︙ | ︙ | |||
64 65 66 67 68 69 70 | origZettel, err := getOrigZettel(ctx, w, r, getZettel, "New") if err != nil { wui.reportError(ctx, w, err) return } m := origZettel.Meta title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk) | | | | | | | | | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | origZettel, err := getOrigZettel(ctx, w, r, getZettel, "New") if err != nil { wui.reportError(ctx, w, err) return } m := origZettel.Meta title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk) textTitle, err := adapter.FormatInlines(title, api.EncoderText, nil) if err != nil { wui.reportError(ctx, w, err) return } env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} htmlTitle, err := adapter.FormatInlines(title, api.EncoderHTML, &env) if err != nil { wui.reportError(ctx, w, err) return } wui.renderZettelForm(w, r, newZettel.Run(origZettel), textTitle, htmlTitle) } } func getOrigZettel( ctx context.Context, w http.ResponseWriter, r *http.Request, getZettel usecase.GetZettel, op string, ) (domain.Zettel, error) { if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { return domain.Zettel{}, adapter.NewErrBadRequest( fmt.Sprintf("%v zettel not possible in format %q", op, formatText)) } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { return domain.Zettel{}, box.ErrNotFound } origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { return domain.Zettel{}, box.ErrNotFound } return origZettel, nil } func (wui *WebUI) renderZettelForm( w http.ResponseWriter, r *http.Request, |
︙ | ︙ |
Changes to web/adapter/webui/delete_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | > > < | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Delete zettel not possible in format %q", formatText))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zettel, err := getZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return |
︙ | ︙ | |||
62 63 64 65 66 67 68 | // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } if err := deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('/')) } } |
Changes to web/adapter/webui/edit_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | > > < | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, err) return } if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Edit zettel %q not possible in format %q", zid, formatText))) return } user := wui.getUser(ctx) m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base) |
︙ | ︙ | |||
66 67 68 69 70 71 72 | // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { | | | 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zettel, hasContent, err := parseZettelForm(r, zid) if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form")) return |
︙ | ︙ |
Changes to web/adapter/webui/get_info.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" | > > > > < | > > > > | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "fmt" "net/http" "sort" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) type metaDataInfo struct { Key string Value string } type matrixElement struct { Text string HasURL bool URL string } type matrixLine struct { Elements []matrixElement } // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler( parseZettel usecase.ParseZettel, getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() if format, formatText := adapter.GetFormat(r, q, api.EncoderHTML); format != api.EncoderHTML { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Zettel info not available in format %q", formatText))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { wui.reportError(ctx, w, err) return |
︙ | ︙ | |||
74 75 76 77 78 79 80 | metaData := make([]metaDataInfo, len(pairs)) getTitle := makeGetTitle(ctx, getMeta, &env) for i, p := range pairs { var html strings.Builder wui.writeHTMLMetaValue(&html, zn.Meta, p.Key, getTitle, &env) metaData[i] = metaDataInfo{p.Key, html.String()} } | > | > | | | | | | | | | | | | | | | | | | | | | > > | | | | | | | | | | | | | | | | | | | | | | > > | | 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | metaData := make([]metaDataInfo, len(pairs)) getTitle := makeGetTitle(ctx, getMeta, &env) for i, p := range pairs { var html strings.Builder wui.writeHTMLMetaValue(&html, zn.Meta, p.Key, getTitle, &env) metaData[i] = metaDataInfo{p.Key, html.String()} } shadowLinks := getShadowLinks(ctx, zid, getAllMeta) endnotes, err := formatBlocks(nil, api.EncoderHTML, &env) if err != nil { endnotes = "" } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := wui.getUser(ctx) canCreate := wui.canCreate(ctx, user) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool EditURL string CanFolge bool FolgeURL string CanCopy bool CopyURL string CanRename bool RenameURL string CanDelete bool DeleteURL string MetaData []metaDataInfo HasLinks bool HasLocLinks bool LocLinks []localLink HasExtLinks bool ExtLinks []string ExtNewWindow string Matrix []matrixLine HasShadowLinks bool ShadowLinks []string Endnotes string }{ Zid: zid.String(), WebURL: wui.NewURLBuilder('h').SetZid(zid).String(), ContextURL: wui.NewURLBuilder('j').SetZid(zid).String(), CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), CanRename: wui.canRename(ctx, user, zn.Meta), RenameURL: wui.NewURLBuilder('b').SetZid(zid).String(), CanDelete: wui.canDelete(ctx, user, zn.Meta), DeleteURL: wui.NewURLBuilder('d').SetZid(zid).String(), MetaData: metaData, HasLinks: len(extLinks)+len(locLinks) > 0, HasLocLinks: len(locLinks) > 0, LocLinks: locLinks, HasExtLinks: len(extLinks) > 0, ExtLinks: extLinks, ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), Matrix: wui.infoAPIMatrix(zid), HasShadowLinks: len(shadowLinks) > 0, ShadowLinks: shadowLinks, Endnotes: endnotes, }) } } type localLink struct { Valid bool Zid string |
︙ | ︙ | |||
160 161 162 163 164 165 166 | locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()}) } return locLinks, extLinks } func (wui *WebUI) infoAPIMatrix(zid id.Zid) []matrixLine { formats := encoder.GetFormats() | > > > > > | | | | | > > > > > > > > > > > > > > | 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 | locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()}) } return locLinks, extLinks } func (wui *WebUI) infoAPIMatrix(zid id.Zid) []matrixLine { formats := encoder.GetFormats() formatTexts := make([]string, 0, len(formats)) for _, f := range formats { formatTexts = append(formatTexts, f.String()) } sort.Strings(formatTexts) defFormat := encoder.GetDefaultFormat().String() parts := []string{"zettel", "meta", "content"} matrix := make([]matrixLine, 0, len(parts)) u := wui.NewURLBuilder('z').SetZid(zid) for _, part := range parts { row := make([]matrixElement, 0, len(formatTexts)+1) row = append(row, matrixElement{part, false, ""}) for _, format := range formatTexts { u.AppendQuery(api.QueryKeyPart, part) if format != defFormat { u.AppendQuery(api.QueryKeyFormat, format) } row = append(row, matrixElement{format, true, u.String()}) u.ClearQuery() } matrix = append(matrix, matrixLine{row}) } return matrix } func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string { ml, err := getAllMeta.Run(ctx, zid) if err != nil || len(ml) < 2 { return nil } result := make([]string, 0, len(ml)-1) for _, m := range ml[1:] { if boxNo, ok := m.Get(meta.KeyBoxNumber); ok { result = append(result, boxNo) } } return result } |
Changes to web/adapter/webui/get_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 | package webui import ( "bytes" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" | > > < | | | | | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | package webui import ( "bytes" "net/http" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } syntax := r.URL.Query().Get("syntax") zn, err := parseZettel.Run(ctx, zid, syntax) if err != nil { wui.reportError(ctx, w, err) return } lang := config.GetLang(zn.InhMeta, wui.rtConfig) envHTML := encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, wui, 'h', getMeta, "", api.EncoderUnknown), ImageAdapter: adapter.MakeImageAdapter(ctx, wui, getMeta), CiteAdapter: nil, Lang: lang, Xhtml: false, MarkerExternal: wui.rtConfig.GetMarkerExternal(), NewWindow: true, IgnoreMeta: map[string]bool{meta.KeyTitle: true, meta.KeyLang: true}, } metaHeader, err := formatMeta(zn.InhMeta, api.EncoderHTML, &envHTML) if err != nil { wui.reportError(ctx, w, err) return } htmlTitle, err := adapter.FormatInlines( encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), api.EncoderHTML, &envHTML) if err != nil { wui.reportError(ctx, w, err) return } htmlContent, err := formatBlocks(zn.Ast, api.EncoderHTML, &envHTML) if err != nil { wui.reportError(ctx, w, err) return } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := wui.getUser(ctx) roleText := zn.Meta.GetDefault(meta.KeyRole, "*") tags := wui.buildTagInfos(zn.Meta) canCreate := wui.canCreate(ctx, user) getTitle := makeGetTitle(ctx, getMeta, &encoder.Environment{Lang: lang}) extURL, hasExtURL := zn.Meta.Get(meta.KeyURL) backLinks := wui.formatBackLinks(zn.InhMeta, getTitle) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) base.MetaHeader = metaHeader wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { |
︙ | ︙ | |||
109 110 111 112 113 114 115 | EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, | | | | | | 111 112 113 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 | EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), FolgeRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle), PrecursorRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(envHTML.NewWindow && hasExtURL), Content: htmlContent, HasBackLinks: len(backLinks) > 0, BackLinks: backLinks, }) } } func formatBlocks(bs ast.BlockSlice, format api.EncodingEnum, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", adapter.ErrNoSuchFormat } var content strings.Builder _, err := enc.WriteBlocks(&content, bs) if err != nil { return "", err } return content.String(), nil } func formatMeta(m *meta.Meta, format api.EncodingEnum, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", adapter.ErrNoSuchFormat } var content strings.Builder _, err := enc.WriteMeta(&content, m) |
︙ | ︙ | |||
186 187 188 189 190 191 192 | } result := make([]simpleLink, 0, len(values)) for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } | | | 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | } result := make([]simpleLink, 0, len(values)) for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTitle(zid, api.EncoderText); found > 0 { url := wui.NewURLBuilder('h').SetZid(zid).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) } } } return result } |
Changes to web/adapter/webui/home.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package webui import ( "context" "errors" "net/http" | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | package webui import ( "context" "errors" "net/http" "zettelstore.de/z/box" "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 (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if r.URL.Path != "/" { wui.reportError(ctx, w, box.ErrNotFound) return } homeZid := wui.rtConfig.GetHomeZettel() if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } homeZid = id.DefaultHomeZid } _, err := s.GetMeta(ctx, homeZid) if err == nil { redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil { redirectFound(w, r, wui.NewURLBuilder('a')) return } redirectFound(w, r, wui.NewURLBuilder('h')) } } |
Changes to web/adapter/webui/htmlmeta.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 | "context" "errors" "fmt" "io" "net/url" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" | > > < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | "context" "errors" "fmt" "io" "net/url" "time" "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) var space = []byte{' '} |
︙ | ︙ | |||
42 43 44 45 46 47 48 | case meta.TypeID: wui.writeIdentifier(w, m.GetDefault(key, "???i"), getTitle) case meta.TypeIDSet: if l, ok := m.GetList(key); ok { wui.writeIdentifierSet(w, l, getTitle) } case meta.TypeNumber: | | < < | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | case meta.TypeID: wui.writeIdentifier(w, m.GetDefault(key, "???i"), getTitle) case meta.TypeIDSet: if l, ok := m.GetList(key); ok { wui.writeIdentifierSet(w, l, getTitle) } case meta.TypeNumber: wui.writeNumber(w, key, m.GetDefault(key, "???n")) case meta.TypeString: writeString(w, m.GetDefault(key, "???s")) case meta.TypeTagSet: if l, ok := m.GetList(key); ok { wui.writeTagSet(w, key, l) } case meta.TypeTimestamp: if ts, ok := m.GetTime(key); ok { writeTimestamp(w, ts) } case meta.TypeURL: writeURL(w, m.GetDefault(key, "???u")) case meta.TypeWord: wui.writeWord(w, key, m.GetDefault(key, "???w")) case meta.TypeWordSet: if l, ok := m.GetList(key); ok { wui.writeWordSet(w, key, l) } case meta.TypeZettelmarkup: writeZettelmarkup(w, m.GetDefault(key, "???z"), env) default: strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false) fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key) } } func (wui *WebUI) writeHTMLBool(w io.Writer, key string, val bool) { |
︙ | ︙ | |||
87 88 89 90 91 92 93 | strfun.HTMLEscape(w, val, false) } func writeEmpty(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } | | | | | | < < < < | | | | | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | strfun.HTMLEscape(w, val, false) } func writeEmpty(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTitle getTitleFunc) { zid, err := id.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } title, found := getTitle(zid, api.EncoderText) switch { case found > 0: if title == "" { fmt.Fprintf(w, "<a href=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), zid) } else { fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), title, zid) } case found == 0: fmt.Fprintf(w, "<s>%v</s>", val) case found < 0: io.WriteString(w, val) } } func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTitle getTitleFunc) { for i, val := range vals { if i > 0 { w.Write(space) } wui.writeIdentifier(w, val, getTitle) } } func (wui *WebUI) writeNumber(w io.Writer, key, val string) { wui.writeLink(w, key, val, val) } func writeString(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) { for i, tag := range tags { if i > 0 { w.Write(space) } wui.writeLink(w, key, tag, tag) } } func writeTimestamp(w io.Writer, ts time.Time) { io.WriteString(w, ts.Format("2006-01-02 15:04:05")) } func writeURL(w io.Writer, val string) { u, err := url.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } fmt.Fprintf(w, "<a href=\"%v\"%v>", u, htmlAttrNewWindow(true)) strfun.HTMLEscape(w, val, false) io.WriteString(w, "</a>") } func (wui *WebUI) writeWord(w io.Writer, key, word string) { wui.writeLink(w, key, word, word) } func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) { for i, word := range words { if i > 0 { w.Write(space) } wui.writeWord(w, key, word) } } func writeZettelmarkup(w io.Writer, val string, env *encoder.Environment) { title, err := adapter.FormatInlines(parser.ParseMetadata(val), api.EncoderHTML, env) if err != nil { strfun.HTMLEscape(w, val, false) return } io.WriteString(w, title) } func (wui *WebUI) writeLink(w io.Writer, key, value, text string) { fmt.Fprintf(w, "<a href=\"%v?%v=%v\">", wui.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value)) strfun.HTMLEscape(w, text, false) io.WriteString(w, "</a>") } type getTitleFunc func(id.Zid, api.EncodingEnum) (string, int) func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, env *encoder.Environment) getTitleFunc { return func(zid id.Zid, format api.EncodingEnum) (string, int) { m, err := getMeta.Run(box.NoEnrichContext(ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } astTitle := parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")) title, err := adapter.FormatInlines(astTitle, format, env) if err == nil { return title, 1 } return "", 1 } } |
Changes to web/adapter/webui/lists.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 | "context" "net/http" "net/url" "sort" "strconv" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" | > > < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | "context" "net/http" "net/url" "sort" "strconv" "strings" "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/search" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of // zettel as HTML. |
︙ | ︙ | |||
49 50 51 52 53 54 55 | } } func (wui *WebUI) renderZettelList(w http.ResponseWriter, r *http.Request, listMeta usecase.ListMeta) { query := r.URL.Query() s := adapter.GetSearch(query, false) ctx := r.Context() | | | | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | } } func (wui *WebUI) renderZettelList(w http.ResponseWriter, r *http.Request, listMeta usecase.ListMeta) { query := r.URL.Query() s := adapter.GetSearch(query, false) ctx := r.Context() title := wui.listTitleSearch("Select", s) wui.renderMetaList( ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = box.NoEnrichContext(ctx) } return listMeta.Run(ctx, s) }, func(offset int) string { return wui.newPageURL('h', query, offset, "_offset", "_limit") }) } |
︙ | ︙ | |||
183 184 185 186 187 188 189 | return } title := wui.listTitleSearch("Search", s) wui.renderMetaList( ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { | | | | | | | | | | 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 | return } title := wui.listTitleSearch("Search", s) wui.renderMetaList( ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = box.NoEnrichContext(ctx) } return ucSearch.Run(ctx, s) }, func(offset int) string { return wui.newPageURL('f', query, offset, "offset", "limit") }) } } // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } q := r.URL.Query() dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) depth := getIntParameter(q, api.QueryKeyDepth, 5) limit := getIntParameter(q, api.QueryKeyLimit, 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return } metaLinks, err := wui.buildHTMLMetaList(metaList) if err != nil { adapter.InternalServerError(w, "Build HTML meta list", err) return } depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"} depthLinks := make([]simpleLink, len(depths)) depthURL := wui.NewURLBuilder('j').SetZid(zid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward) case usecase.ZettelContextForward: depthURL.AppendQuery(api.QueryKeyDir, api.DirForward) } depthURL.AppendQuery(api.QueryKeyDepth, depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData user := wui.getUser(ctx) wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { |
︙ | ︙ | |||
267 268 269 270 271 272 273 | ctx context.Context, w http.ResponseWriter, title string, s *search.Search, ucMetaList func(sorter *search.Search) ([]*meta.Meta, error), pageURL func(int) string) { | < < < < < < < < | | | | < < < < < < < < < < < < < < < < < < | | < < < < < | | < < < < < | 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 | ctx context.Context, w http.ResponseWriter, title string, s *search.Search, ucMetaList func(sorter *search.Search) ([]*meta.Meta, error), pageURL func(int) string) { metaList, err := ucMetaList(s) if err != nil { wui.reportError(ctx, w, err) return } user := wui.getUser(ctx) metas, err := wui.buildHTMLMetaList(metaList) if err != nil { wui.reportError(ctx, w, err) return } var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { Title string Metas []simpleLink }{ Title: title, Metas: metas, }) } func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string { if s == nil { return wui.rtConfig.GetSiteName() } |
︙ | ︙ | |||
366 367 368 369 370 371 372 | if val, ok := m.Get(meta.KeyLang); ok { lang = val } else { lang = defaultLang } title, _ := m.Get(meta.KeyTitle) env := encoder.Environment{Lang: lang, Interactive: true} | | | 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | if val, ok := m.Get(meta.KeyLang); ok { lang = val } else { lang = defaultLang } title, _ := m.Get(meta.KeyTitle) env := encoder.Environment{Lang: lang, Interactive: true} htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), api.EncoderHTML, &env) if err != nil { return nil, err } metas = append(metas, simpleLink{ Text: htmlTitle, URL: wui.NewURLBuilder('h').SetZid(m.Zid).String(), }) } return metas, nil } |
Changes to web/adapter/webui/login.go.
︙ | ︙ | |||
36 37 38 39 40 41 42 | Retry bool }{ Title: base.Title, Retry: retry, }) } | | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | Retry bool }{ Title: base.Title, Retry: retry, }) } // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. func (wui *WebUI) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { redirectFound(w, r, wui.NewURLBuilder('/')) return } ctx := r.Context() ident, cred, ok := adapter.GetCredentialsViaForm(r) |
︙ | ︙ |
Changes to web/adapter/webui/rename_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | > > < | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), formatText))) return } user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), user, &base) wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { |
︙ | ︙ | |||
62 63 64 65 66 67 68 | // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } if err = r.ParseForm(); err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) return } |
︙ | ︙ |
Changes to web/adapter/webui/response.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 | // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" "zettelstore.de/z/api" ) func redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { http.Redirect(w, r, ub.String(), http.StatusFound) } |
Changes to web/adapter/webui/webui.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "bytes" "context" "log" "net/http" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" | > > < | > | | | | | | > | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | "bytes" "context" "log" "net/http" "sync" "time" "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // WebUI holds all data for delivering the web ui. type WebUI struct { ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox policy auth.Policy templateCache map[id.Zid]*template.Template mxCache sync.RWMutex tokenLifetime time.Duration cssBaseURL string cssUserURL string homeURL string listZettelURL string listRolesURL string listTagsURL string withAuth bool loginURL string searchURL string } type webuiBox interface { CanCreateZettel(ctx context.Context) bool GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool AllowRenameZettel(ctx context.Context, zid id.Zid) bool CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // New creates a new WebUI struct. func New(ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy) *WebUI { wui := &WebUI{ ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), cssBaseURL: ab.NewURLBuilder('z').SetZid( id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(), cssUserURL: ab.NewURLBuilder('z').SetZid( id.UserCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), listRolesURL: ab.NewURLBuilder('h').AppendQuery("_l", "r").String(), listTagsURL: ab.NewURLBuilder('h').AppendQuery("_l", "t").String(), withAuth: authz.WithAuth(), loginURL: ab.NewURLBuilder('a').String(), searchURL: ab.NewURLBuilder('f').String(), } wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) observe(ci box.UpdateInfo) { wui.mxCache.Lock() if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid { wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache)) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() } |
︙ | ︙ | |||
116 117 118 119 120 121 122 | t, ok := wui.templateCache[zid] wui.mxCache.RUnlock() return t, ok } func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) | | | | | | | | | > | | | | | | | | | | | | < | | | | | < | | | < < < < > | > | | | | > > > | | < < | | | | 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 | t, ok := wui.templateCache[zid] wui.mxCache.RUnlock() return t, ok } func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx) } func (wui *WebUI) canWrite( ctx context.Context, user, meta *meta.Meta, content domain.Content) bool { return wui.policy.CanWrite(user, meta, meta) && wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) } func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid) } func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid) } func (wui *WebUI) getTemplate( ctx context.Context, templateID id.Zid) (*template.Template, error) { if t, ok := wui.cacheGetTemplate(templateID); ok { return t, nil } realTemplateZettel, err := wui.box.GetZettel(ctx, templateID) if err != nil { return nil, err } t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) if err == nil { // t.SetErrorOnMissing() wui.cacheSetTemplate(templateID, t) } return t, err } type simpleLink struct { Text string URL string } type baseData struct { Lang string MetaHeader string CSSBaseURL string CSSUserURL string Title string HomeURL string WithUser bool WithAuth bool UserIsValid bool UserZettelURL string UserIdent string UserLogoutURL string LoginURL string ListZettelURL string ListRolesURL string ListTagsURL string HasNewZettelLinks bool NewZettelLinks []simpleLink SearchURL string Content string FooterHTML string } func (wui *WebUI) makeBaseData( ctx context.Context, lang, title string, user *meta.Meta, data *baseData) { var ( userZettelURL string userIdent string userLogoutURL string ) userIsValid := user != nil if userIsValid { userZettelURL = wui.NewURLBuilder('h').SetZid(user.Zid).String() userIdent = user.GetDefault(meta.KeyUserID, "") userLogoutURL = wui.NewURLBuilder('a').SetZid(user.Zid).String() } newZettelLinks := wui.fetchNewTemplates(ctx, user) data.Lang = lang data.CSSBaseURL = wui.cssBaseURL data.CSSUserURL = wui.cssUserURL data.Title = title data.HomeURL = wui.homeURL data.WithAuth = wui.withAuth data.WithUser = data.WithAuth data.UserIsValid = userIsValid data.UserZettelURL = userZettelURL data.UserIdent = userIdent data.UserLogoutURL = userLogoutURL data.LoginURL = wui.loginURL data.ListZettelURL = wui.listZettelURL data.ListRolesURL = wui.listRolesURL data.ListTagsURL = wui.listTagsURL data.HasNewZettelLinks = len(newZettelLinks) > 0 data.NewZettelLinks = newZettelLinks data.SearchURL = wui.searchURL data.FooterHTML = wui.rtConfig.GetFooterHTML() } // htmlAttrNewWindow returns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { if hasURL { return " target=\"_blank\" ref=\"noopener noreferrer\"" } return "" } func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) { ctx = box.NoEnrichContext(ctx) if !wui.canCreate(ctx, user) { return nil } menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig)) for _, ref := range refs { zid, err := id.Parse(ref.URL.Path) if err != nil { continue } m, err := wui.box.GetMeta(ctx, zid) if err != nil { continue } if !wui.policy.CanRead(user, m) { continue } title := config.GetTitle(m, wui.rtConfig) astTitle := parser.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} menuTitle, err := adapter.FormatInlines(astTitle, api.EncoderHTML, &env) if err != nil { menuTitle, err = adapter.FormatInlines(astTitle, api.EncoderText, nil) if err != nil { menuTitle = title } } result = append(result, simpleLink{ Text: menuTitle, URL: wui.NewURLBuilder('g').SetZid(m.Zid).String(), |
︙ | ︙ | |||
319 320 321 322 323 324 325 | wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { base.Content = content.String() | | | | 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 | wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { base.Content = content.String() w.Header().Set(api.HeaderContentType, "text/html; charset=utf-8") w.WriteHeader(code) err = bt.Render(w, base) } if err != nil { log.Println("Unable to render template", err) } } func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) } // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) } func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context { return wui.ab.ClearToken(ctx, w) } func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) { wui.ab.SetToken(w, token, wui.tokenLifetime) } |
Changes to web/server/impl/impl.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package impl import ( "context" "net/http" "time" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) type myServer struct { server httpServer | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package impl import ( "context" "net/http" "time" "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) type myServer struct { server httpServer |
︙ | ︙ | |||
53 54 55 56 57 58 59 | } func (srv *myServer) GetUser(ctx context.Context) *meta.Meta { if data := srv.GetAuthData(ctx); data != nil { return data.User } return nil } | | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | } func (srv *myServer) GetUser(ctx context.Context) *meta.Meta { if data := srv.GetAuthData(ctx); data != nil { return data.User } return nil } func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(srv.GetURLPrefix(), key) } func (srv *myServer) GetURLPrefix() string { return srv.router.urlPrefix } const sessionName = "zsession" |
︙ | ︙ |
Changes to web/server/impl/router.go.
︙ | ︙ | |||
96 97 98 99 100 101 102 | if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } r.URL.Path = r.URL.Path[prefixLen-1:] } match := rt.reURL.FindStringSubmatch(r.URL.Path) | | > > > > | | | | | | | | | | | < < | < < > | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } r.URL.Path = r.URL.Path[prefixLen-1:] } match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) != 3 { rt.mux.ServeHTTP(w, rt.addUserContext(r)) return } key := match[1][0] table := rt.zettelTable if match[2] == "" { table = rt.listTable } if mh, ok := table[key]; ok { if handler, ok := mh[r.Method]; ok { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, rt.addUserContext(r)) return } } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { return r } k := auth.KindJSON |
︙ | ︙ |
Deleted web/server/impl/urlbuilder.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/server/server.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 | package server import ( "context" "net/http" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) | > < < < < < < < < < < < < < < < < < < < < < < < < | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | package server import ( "context" "net/http" "time" "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } // Router allows to state routes for various URL paths. type Router interface { Handle(pattern string, handler http.Handler) AddListRoute(key byte, httpMethod string, handler http.Handler) AddZettelRoute(key byte, httpMethod string, handler http.Handler) SetUserRetriever(ur UserRetriever) } // Builder allows to build new URLs for the web service. type Builder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder } // Auth is the authencation interface. type Auth interface { GetUser(context.Context) *meta.Meta SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context |
︙ | ︙ |
Changes to www/build.md.
︙ | ︙ | |||
31 32 33 34 35 36 37 | ``` The flag `-v` enables the verbose mode. It outputs all commands called by the tool. `COMMAND` is one of: | | | | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | ``` The flag `-v` enables the verbose mode. It outputs all commands called by the tool. `COMMAND` is one of: * `build`: builds the software with correct version information and puts it into a freshly created directory <tt>bin</tt>. * `check`: checks the current state of the working directory to be ready for release (or commit). * `release`: executes `check` command and if this was successful, builds the software for various platforms, and creates ZIP files for each executable. Everything is put in the directory <tt>releases</tt>. * `clean`: removes the directories <tt>bin</tt> and <tt>releases</tt>. * `version`: prints the current version information. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command ``` |
︙ | ︙ |
Changes to www/changes.wiki.
1 2 3 | <title>Change Log</title> <a name="0_0_14"></a> | > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | < | | | | | | | | > | | | | > | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 | <title>Change Log</title> <a name="0_0_15"></a> <h2>Changes for Version 0.0.15 (pending)</h2> <a name="0_0_14"></a> <h2>Changes for Version 0.0.14 (2021-07-23)</h2> * Rename “place” into “box”. This also affects the configuration keys to specify boxes <tt>box-uri<em>X</em></tt> (previously <tt>place-uri-<em>X</em></tt>. Older changes documented here are renamed too. (breaking) * Add API for creating, updating, renaming, and deleting zettel. (major: api) * Initial API client for Go. (major: api) * Remove support for paging of WebUI list. Runtime configuration key <tt>list-page-size</tt> is removed. If you still specify it, it will be ignored. (major: webui) * Use endpoint <tt>/v</tt> for user authentication via API. Endpoint <tt>/a</tt> is now used for the web user interface only. Similar, endpoint <tt>/y</tt> (“zettel context”) is renamed to <tt>/x</tt>. (minor, possibly breaking) * Type of used-defined metadata is determined by suffix of key: <tt>-number</tt>, <tt>-url</tt>, <tt>-zid</tt> will result the values to be interpreted as a number, an URL, or a zettel identifier. (minor, but possibly breaking if you already used a metadata key with above suffixes, but as a string type) * New <tt>user-role</tt> “creator”, which is only allowed to create new zettel (except user zettel). This role may only read and update public zettel or its own user zettel. Added to support future client software (e.g. on a mobile device) that automatically creates new zettel but, in case of a password loss, should not allow to read existing zettel. (minor, possibly breaking, because new zettel template zettel must always prepend the string <tt>new-</tt> before metdata keys that should be transferred to the new zettel) * New suported metadata key <tt>box-number</tt>, which gives an indication from which box the zettel was loaded. (minor) * New supported syntax <tt>html</tt>. (minor) * New predefined zettel “User CSS” that can be used to redefine some predefined CSS (without modifying the base CSS zettel). (minor: webui) * When a user moves a zettel file with additional characters into the box directory, these characters are preserved when zettel is updated. (bug) * The phase “filtering a zettel list” is more precise “selecting zettel” (documentation) * Many smaller bug fixes and inprovements, to the software and to the documentation. <a name="0_0_13"></a> <h2>Changes for Version 0.0.13 (2021-06-01)</h2> * Startup configuration <tt>box-<em>X</em>-uri</tt> (where <em>X</em> is a number greater than zero) has been renamed to <tt>box-uri-<em>X</em></tt>. (breaking) * Web server processes startup configuration <tt>url-prefix</tt>. There is no need for stripping the prefix by a front-end web server any more. (breaking: webui, api) * Administrator console (only optional accessible locally). Enable it only on systems with a single user or with trusted users. It is disabled by default. (major: core) * Remove visibility value “simple-expert” introduced in [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There was a name collision with the “simple” directory box sub-type. (major) * For security reasons, HTML blocks are not encoded as HTML if they contain certain snippets, such as <tt><script</tt> or <tt><iframe</tt>. These may be caused by using CommonMark as a zettel syntax. (major) * Full-text search can be a prefix search or a search for equal words, in addition to the search whether a word just contains word of the search term. (minor: api, webui) * Full-text search for URLs, with above additional operators. (minor: api, webui) * Add system zettel about license, contributors, and dependencies (and their license). For a nicer layout of zettel identifier, the zettel about environment values and about runtime metrics got new zettel identifier. This affects only user that referenced those zettel. (minor) * Local images that cannot be read (not found or no access rights) are substituted with the new default image, a spinning emoji. See [/file?name=box/constbox/emoji_spin.gif]. (minor: webui) * Add zettelmarkup syntax for a table row that should be ignored: <tt>|%</tt>. This allows to paste output of the administrator console into a zettel. (minor: zmk) * Many smaller bug fixes and inprovements, to the software and to the documentation. <a name="0_0_12"></a> <h2>Changes for Version 0.0.12 (2021-04-16)</h2> * Raise the per-process limit of open files on macOS to 1.048.576. This allows most macOS users to use at least 500.000 zettel. That should be enough for the near future. (major) * Mitigate the shortcomings of the macOS version by introducing types of directory boxes. The original directory box type is now called "notify" (the default value). There is a new type called "simple". This new type does not notify Zettelstore when some of the underlying Zettel files change. (major) * Add new startup configuration <tt>default-dir-box-type</tt>, which gives the default value for specifying a directory box type. The default value is “notify”. On macOS, the default value may be changed “simple” if some errors occur while raising the per-process limit of open files. (minor) <a name="0_0_11"></a> <h2>Changes for Version 0.0.11 (2021-04-05)</h2> * New box schema "file" allows to read zettel from a ZIP file. A zettel collection can now be packaged and distributed easier. (major: server) * Non-restricted search is a full-text search. The search string will be normalized according to Unicode NFKD. Every character that is not a letter or a number will be ignored for the search. It is sufficient if the words to be searched are part of words inside a zettel, both content and metadata. (major: api, webui) * A zettel can be excluded from being indexed (and excluded from being found in a search) if it contains the metadata <tt>no-index: true</tt>. (minor: api, webui) * Menu bar is shown when displaying error messages. (minor: webui) * When selecting zettel, it can be specified that a given value should <em>not</em> match. Previously, only the whole select criteria could be negated (which is still possible). (minor: api, webui) * You can select a zettel by specifying that specific metadata keys must (or must not) be present. (minor: api, webui) * Context of a zettel (introduced in version 0.0.10) does not take tags into account any more. Using some tags for determining the context resulted into erratic, non-deterministic context lists. (minor: api, webui) * Selecting zettel depending on tag values can be both by comparing only the prefix or the whole string. If a search value begins with '#', only zettel with the exact tag will be returned. Otherwise a zettel will be returned if the search string just matches the prefix of only one of its tags. (minor: api, webui) * Many smaller bug fixes and inprovements, to the software and to the documentation. A note for users of macOS: in the current release and with macOS's default values, a zettel directory must not contain more than approx. 250 files. There are three options to mitigate this limitation temporarily: # You update the per-process limit of open files on macOS. # You setup a virtualization environment to run Zettelstore on Linux or Windows. # You wait for version 0.0.12 which addresses this issue. <a name="0_0_10"></a> <h2>Changes for Version 0.0.10 (2021-02-26)</h2> * Menu item “Home” now redirects to a home zettel. |
︙ | ︙ | |||
130 131 132 133 134 135 136 | <b>Please update your zettel if you make use of the now deprecated feature.</b> (major: webui) * A reference that starts with two slash characters (“<code>//</code>”) it will be interpreted relative to the value of <code>url-prefix</code>. For example, if <code>url-prefix</code> has the value <code>/manual/</code>, the reference <code>[[Zettel list|//h]]</code> will render as <code><a href="/manual/h">Zettel list</a></code>. (minor: syntax) | | | | | | > | | > | | > | | > | | | | > | > | > | | | > | > | | > | > | | > | > | | | > | | > | > | > | | | | | > | | > | > > | > | > | | > | | > | | | | < | > > | > > | > | | > | > | | > | > | | > | > | | | > | | > | > | > | | | > | | | > | > | > | > > | | > | > | > | > | | > | | | | | | > | > | | > | | | > | > | | > > | | > | | | > > | > | > | | | | > | > | | > | | > > > | | > | > | > | | | | | > | > | > | | > > | > | > | > > | > | | > | > | | > | | | 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 | <b>Please update your zettel if you make use of the now deprecated feature.</b> (major: webui) * A reference that starts with two slash characters (“<code>//</code>”) it will be interpreted relative to the value of <code>url-prefix</code>. For example, if <code>url-prefix</code> has the value <code>/manual/</code>, the reference <code>[[Zettel list|//h]]</code> will render as <code><a href="/manual/h">Zettel list</a></code>. (minor: syntax) * Searching/selecting ignores the leading '#' character of tags. (minor: api, webui) * When result of selecting or searching is presented, the query is written as the page heading. (minor: webui) * A reference to a zettel that contains a URL fragment, will now be processed by the indexer. (bug: server) * Runtime configuration key <tt>marker-external</tt> now defaults to “&#10138;” (“➚”). It is more beautiful than the previous “&#8599;&#xfe0e;” (“↗︎”), which also needed the additional “&#xfe0e;” to disable the conversion to an emoji on iPadOS. (minor: webui) * A pre-build binary for macOS ARM64 (also known as Apple silicon) is available. (minor: infrastructure) * Many smaller bug fixes and inprovements, to the software and to the documentation. <a name="0_0_9"></a> <h2>Changes for Version 0.0.9 (2021-01-29)</h2> This is the first version that is managed by [https://fossil-scm.org|Fossil] instead of GitHub. To access older versions, use the Git repository under [https://github.com/zettelstore/zettelstore-github|zettelstore-github]. <h3>Server / API</h3> * (major) Support for property metadata. Metadata key <tt>published</tt> is the first example of such a property. * (major) A background activity (called <i>indexer</i>) continuously monitors zettel changes to establish the reverse direction of found internal links. This affects the new metadata keys <tt>precursor</tt> and <tt>folge</tt>. A user specifies the precursor of a zettel and the indexer computes the property metadata for [https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel]. Metadata keys with type “Identifier” or “IdentifierSet” that have no inverse key (like <tt>precursor</tt> and <tt>folge</tt> with add to the key <tt>forward</tt> that also collects all internal links within the content. The computed inverse is <tt>backward</tt>, which provides all backlinks. The key <tt>back</tt> is computed as the value of <tt>backward</tt>, but without forward links. Therefore, <tt>back</tt> is something like the list of “smart backlinks”. * (minor) If Zettelstore is being stopped, an appropriate message is written in the console log. * (minor) New computed zettel with environmental data, the list of supported meta data keys, and statistics about all configured zettel boxes. Some other computed zettel got a new identifier (to make room for other variant). * (minor) Remove zettel <tt>00000000000004</tt>, which contained the Go version that produced the Zettelstore executable. It was too specific to the current implementation. This information is now included in zettel <tt>00000000000006</tt> (<i>Zettelstore Environment Values</i>). * (minor) Predefined templates for new zettel do not contain any value for attribute <tt>visibility</tt> any more. * (minor) Add a new metadata key type called “Zettelmarkup”. It is a non-empty string, that will be formatted with Zettelmarkup. <tt>title</tt> and <tt>default-title</tt> have this type. * (major) Rename zettel syntax “meta” to “none”. Please update the <i>Zettelstore Runtime Configuration</i> and all other zettel that previously used the value “meta”. Other zettel are typically user zettel, used for authentication. However, there is no real harm, if you do not update these zettel. In this case, the metadata is just not presented when rendered. Zettelstore will still work. * (minor) Login will take at least 500 milliseconds to mitigate login attacks. This affects both the API and the WebUI. * (minor) Add a sort option “_random” to produce a zettel list in random order. <tt>_order</tt> / <tt>order</tt> are now an aliases for the query parameters <tt>_sort</tt> / <tt>sort</tt>. <h3>WebUI</h3> * (major) HTML template zettel for WebUI now use [https://mustache.github.io/|Mustache] syntax instead of previously used [https://golang.org/pkg/html/template/|Go template] syntax. This allows these zettel to be used, even when there is another Zettelstore implementation, in another programming language. Mustache is available for approx. 48 programming languages, instead of only one for Go templates. <b>If you modified your templates, you <i>must</i> adapt them to the new syntax. Otherwise the WebUI will not work.</b> * (major) Show zettel identifier of folgezettel and precursor zettel in the header of a rendered zettel. If a zettel has real backlinks, they are shown at the botton of the page (“Additional links to this zettel”). * (minor) All property metadata, even computed metadata is shown in the info page of a zettel. * (minor) Rendering of metadata keys <tt>title</tt> and <tt>default-title</tt> in info page changed to a full HTML output for these Zettelmarkup encoded values. * (minor) Always show the zettel identifier on the zettel detail view. Previously, the identifier was not shown if the zettel was not editable. * (minor) Do not show computed metadata in edit forms anymore. <a 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 box, its metadata are just calculated from the information available. Updated metadata did not find its way into the zettel box, because the <tt>.meta</tt> file was not written. * (bug) If just the <tt>.meta</tt> file was deleted manually, the zettel was assumed to be missing. A workaround is to restart the software. If the <tt>.meta</tt> file is deleted, metadata is now calculated in the same way when the <tt>.meta</tt> file is non-existing at the start of the software. * (bug) A link to the current zettel, only using a fragment (e.g. <code>[[Title|#title]]</code>) is now handled correctly as a zettel link (and not as a link to external material). * (minor) Allow zettel to be marked as “read only”. This is done through the metadata key <tt>read-only</tt>. * (bug) When renaming a zettel, check all boxes for the new zettel identifier, not just the first one. Otherwise it will be possible to shadow a read-only zettel from a next box, effectively modifying it. * (minor) Add support for a configurable default value for metadata key <tt>visibility</tt>. * (bug) If <tt>list-page-size</tt> is set to a relatively small value and the authenticated user is <i>not</i> the owner, some zettel were not shown in the list of zettel or were not returned by the API. * (minor) Add support for new visibility “expert”. An owner becomes an expert, if the runtime configuration key <tt>expert-mode</tt> is set to true. * (major) Add support for computed zettel. These zettel have an identifier less than <tt>0000000000100</tt>. Most of them are only visible, if <tt>expert-mode</tt> is enabled. * (bug) Fixes a memory leak that results in too many open files after approx. 125 reload operations. * (major) Predefined templates for new zettel got an explicit value for visibility: “login”. Please update these zettel if you modified them. * (major) Rename key <tt>readonly</tt> of <i>Zettelstore Startup Configuration</i> to <tt>read-only-mode</tt>. This was done to avoid some confusion with the the zettel metadata key <tt>read-only</tt>. <b>Please adapt your startup configuration. Otherwise your Zettelstore will be accidentally writable.</b> * (minor) References starting with “./” and “../” are treated as a local reference. Previously, only the prefix “/” was treated as a local reference. * (major) Metadata key <tt>modified</tt> will be set automatically to the current local time if a zettel is updated through Zettelstore. <b>If you used that key previously for your own, you should rename it before you upgrade.</b> * (minor) The new visibility value “simple-expert” ensures that many computed zettel are shown for new users. This is to enable them to send useful bug reports. * (minor) When a zettel is stored as a file, its identifier is additionally stored within the metadata. This helps for better robustness in case the file names were corrupted. In addition, there could be a tool that compares the identifier with the file name. <h3>WebUI</h3> * (minor) Remove list of tags in “List Zettel” and search results. There was some feedback that the additional tags were not helpful. * (minor) Move zettel field "role" above "tags" and move "syntax" more to "content". * (minor) Rename zettel operation “clone” to “copy”. * (major) All predefined HTML templates have now a visibility value “expert”. If you want to see them as an non-expert owner, you must temporary enable <tt>expert-mode</tt> and change the <tt>visibility</tt> metadata value. * (minor) Initial support for [https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel]. If you click on “Folge” (detail view or info view), a new zettel is created with a reference (<tt>precursor</tt>) to the original zettel. Title, role, tags, and syntax are copied from the original zettel. * (major) Most predefined zettel have a title prefix of “Zettelstore”. * (minor) If started in simple mode, e.g. via double click or without any command, some information for the new user is presented. In the terminal, there is a hint about opening the web browser and use a specific URL. A <i>Welcome zettel</i> is created, to give some more information. (This change also applies to the server itself, but it is more suited to the WebUI user.) <a name="0_0_7"></a> <h2>Changes for Version 0.0.7 (2020-11-24)</h2> * With this version, Zettelstore and this manual got a new license, the [https://joinup.ec.europa.eu/collection/eupl|European Union Public Licence] (EUPL), version 1.2 or later. Nothing else changed. If you want to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to fork from the previous version. <a name="0_0_6"></a> <h2>Changes for Version 0.0.6 (2020-11-23)</h2> <h3>Server</h3> * (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to <tt>00000000000100</tt> (previously <tt>00000000000001</tt>). This is done to gain some free identifier with smaller number to be used internally. <b>If you customized this zettel, please make sure to rename it to the new identifier.</b> * (major) Rename the two essential metadata keys of a user zettel to <tt>credential</tt> and <tt>user-id</tt>. The previous values were <tt>cred</tt> and <tt>ident</tt>. <b>If you enabled user authentication and added some user zettel, make sure to change them accordingly. Otherwise these users will not authenticated any more.</b> * (minor) Rename the scheme of the box URL where predefined zettel are stored to “const”. The previous value was “globals”. <h3>Zettelmarkup</h3> * (bug) Allow to specify a <i>fragment</i> in a reference to a zettel. Used to link to an internal position within a zettel. This applies to CommonMark too. <h3>API</h3> * (bug) Encoding binary content in format “json” now results in valid JSON content. * (bug) All query parameters of selecting zettel must be true, regardless if a specific key occurs more than one or not. * (minor) Encode all inherited meta values in all formats except “raw”. A meta value is called <i>inherited</i> if there is a key starting with <tt>default-</tt> in the <i>Zettelstore Runtime Configuration</i>. Applies to WebUI also. * (minor) Automatic calculated identifier for headings (only for “html”, “djson”, “native” format and for the Web user interface). You can use this to provide a zettel reference that links to the heading, without specifying an explicit mark (<code>[!mark]</code>). * (major) Allow to retrieve all references of a given zettel. <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 putd after an external URL to <tt>&\#8599;&\#xfe0e;</tt> (“↗︎”). This affects the former key <tt>icon-material</tt> of the <i>Zettelstore Runtime Configuration</i>, which is renamed to <tt>marker-external</tt>. * (major) Allow multiple zettel to act as templates for creating new zettel. All zettel with a role value “new-template” act as a template to create a new zettel. The WebUI menu item “New” changed to a drop-down list with all those zettel, ordered by their identifier. All metadata keys with the prefix <tt>new-</tt> will be translated to a new or updated keys/value without that prefix. You can use this mechanism to specify a role for the new zettel, or a different title. The title of the template zettel is used in the drop-down list. The initial template zettel “New Zettel” has now a different zettel identifier (now: <tt>00000000091001</tt>, was: <tt>00000000040001</tt>). <b>Please update it, if you changed that zettel.</b> <br>Note: this feature was superseded in [#0_0_10|version 0.0.10] by the “New Menu” zettel. * (minor) When a page should be opened in a new windows (e.g. for external references), the web browser is instructed to decouple the new page from the previous one for privacy and security reasons. In detail, the web browser is instructed to omit referrer information and to omit a JS object linking to the page that contained the external link. * (minor) If the value of the <i>Zettelstore Runtime Configuration</i> key <tt>list-page-size</tt> is greater than zero, the number of WebUI list elements will be restricted and it is possible to change to the next/previous page to list more elements. * (minor) Change CSS to enhance reading: make <code>line-height</code> a little smaller (previous: 1.6, now 1.4) and move list items to the left. <a name="0_0_5"></a> <h2>Changes for Version 0.0.5 (2020-10-22)</h2> * Application Programming Interface (API) to allow external software to retrieve zettel data from the Zettelstore. * Specify boxes, where zettel are stored, via an URL. * Add support for a custom footer. <a name="0_0_4"></a> <h2>Changes for Version 0.0.4 (2020-09-11)</h2> * Optional user authentication/authorization. * New sub-commands <tt>file</tt> (use Zettelstore as a command line filter), <tt>password</tt> (for authentication), and <tt>config</tt>. <a name="0_0_3"></a> <h2>Changes for Version 0.0.3 (2020-08-31)</h2> * Starting Zettelstore has been changed by introducing sub-commands. This change is also reflected on the server installation procedures. * Limitations on renaming zettel has been relaxed. <a name="0_0_2"></a> <h2>Changes for Version 0.0.2 (2020-08-28)</h2> * Configuration zettel now has ID <tt>00000000000001</tt> (previously: <tt>00000000000000</tt>). * The zettel with ID <tt>00000000000000</tt> is no longer shown in any zettel list. If you changed the configuration zettel, you should rename it manually in its file directory. * Creating a new zettel is now done by cloning an existing zettel. To mimic the previous behaviour, a zettel with ID <tt>00000000040001</tt> is introduced. You can change it if you need a different template zettel. <a name="0_0_1"></a> <h2>Changes for Version 0.0.1 (2020-08-21)</h2> * Initial public release. |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> | | | | | | | | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.0.14</code> (2021-07-23). * [/uv/zettelstore-0.0.14-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.0.14-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.0.14-windows-amd64.zip|Windows] (amd64) * [/uv/zettelstore-0.0.14-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.0.14-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual-0.0.13.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. |
Changes to www/index.wiki.
︙ | ︙ | |||
13 14 15 16 17 18 19 | 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> | | | | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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.14 (2021-07-23)</h3> * [./download.wiki|Download] * [./changes.wiki#0_0_14|Change summary] * [/timeline?p=version-0.0.14&bt=version-0.0.13&y=ci|Check-ins for version 0.0.14], [/vdiff?to=version-0.0.14&from=version-0.0.13|content diff] * [/timeline?df=version-0.0.14&y=ci|Check-ins derived from the 0.0.14 release], [/vdiff?from=version-0.0.14&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases] <hr> <h2>Build instructions</h2> Just install [https://golang.org/dl/|Go] and some Go-based tools. Please read the [./build.md|instructions] for details. * [/dir?ci=trunk|Source code] * [/download|Download the source code] as a tarball or a ZIP file (you must [/login|login] as user "anonymous"). |
Changes to www/plan.wiki.
1 2 3 4 5 6 7 8 | <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 | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <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 put your file into the directory where your zettel are stored. Make sure that the file name starts with unique 14 digits that make up the zettel identifier. * Automatic lists and transclusions are not supported in Zettelmarkup. * … <h3>Smaller limitations</h3> * Quoted attribute values are not yet supported in Zettelmarkup: <code>{key="value with space"}</code>. * The <tt>file</tt> sub-command currently does not support output format “json”. * The horizontal tab character (<tt>U+0009</tt>) is not supported. * Missing support for citation keys. * Changing the content syntax is not reflected in file extension. * File names with additional text besides the zettel identifier are not always preserved. * … <h3>Planned improvements</h3> * Support for mathematical content is missing, e.g. <code>$$F(x) &= \\int^a_b \\frac{1}{3}x^3$$</code>. * Render zettel in [https://pandoc.org|pandoc's] JSON version of their native AST to make pandoc an external renderer for Zettelstore. * … |